<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Rost</title>
    <description>The latest articles on DEV Community by Rost (@rosgluk).</description>
    <link>https://dev.to/rosgluk</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3544400%2F04dd81bf-749e-4055-971f-316c0134e76c.jpg</url>
      <title>DEV Community: Rost</title>
      <link>https://dev.to/rosgluk</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rosgluk"/>
    <language>en</language>
    <item>
      <title>A2A and MCP Agent Security: Identity, Delegation, and Audit Trails</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Sat, 11 Jul 2026 06:46:43 +0000</pubDate>
      <link>https://dev.to/rosgluk/a2a-and-mcp-agent-security-identity-delegation-and-audit-trails-k3n</link>
      <guid>https://dev.to/rosgluk/a2a-and-mcp-agent-security-identity-delegation-and-audit-trails-k3n</guid>
      <description>&lt;p&gt;Prompt injection gets most of the security attention in LLM systems, and it deserves attention, but it is not the whole problem once agents start calling tools and delegating work to other agents.&lt;/p&gt;

&lt;p&gt;MCP gives an agent structured access to files, APIs, databases, and ticketing systems. A2A lets one agent send tasks, messages, and artifacts to another agent that may belong to a different team, vendor, or runtime. Those protocols are useful precisely because they cross trust boundaries, which means identity, authorization, delegation limits, and audit trails become first-class architecture rather than optional hardening.&lt;/p&gt;

&lt;p&gt;This article is the canonical guide for &lt;strong&gt;agent protocol security&lt;/strong&gt; in the &lt;a href="https://www.glukhov.org/llm-architecture/" rel="noopener noreferrer"&gt;LLM Architecture&lt;/a&gt; cluster. It covers threat models, identity, gateways, registries, delegation, and production checklists. For input validation, output filtering, and prompt safety patterns, see &lt;a href="https://www.glukhov.org/llm-architecture/guardrails/llm-guardrails-in-practice/" rel="noopener noreferrer"&gt;LLM Guardrails in Practice&lt;/a&gt; instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Guardrails vs Protocol Security vs Runtime Policy
&lt;/h2&gt;

&lt;p&gt;These three layers solve different problems and fail in different ways when conflated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LLM guardrails&lt;/strong&gt; operate on model input and output: blocking injection patterns, filtering harmful content, validating JSON shape, and enforcing tone or compliance rules on generated text. They protect the conversation layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Protocol security&lt;/strong&gt; operates on agent boundaries: who may call which MCP tool, which agent may delegate to which peer, what OAuth scopes attach to a task, and whether a downstream agent may act on a user's behalf. It protects the action layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Runtime policy&lt;/strong&gt; sits between them: a policy engine that evaluates requests against rules regardless of whether the trigger was natural language or a structured protocol call. It can require human approval before a tool executes, block egress to unknown domains, or deny delegation when scope exceeds the originating user.&lt;/p&gt;

&lt;p&gt;My opinion is blunt: guardrails without protocol security produce polite chatbots that still exfiltrate data through a tool call. Protocol security without guardrails produces well-authenticated agents that still follow malicious instructions embedded in an artifact. You need both, plus runtime policy for high-risk actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Threat Model for A2A and MCP Agent Systems
&lt;/h2&gt;

&lt;p&gt;Start with assets and adversaries, not with a shopping list of controls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assets worth protecting:&lt;/strong&gt; user data in prompts and artifacts, credentials for MCP servers, production systems reachable through tools, agent reputation, billing accounts tied to token usage, and audit integrity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Realistic adversaries:&lt;/strong&gt; external users abusing public agent endpoints, compromised MCP servers returning poisoned tool results, malicious agents misrepresenting skills in Agent Cards, insiders over-delegating authority, and supply-chain tampering with tool metadata that manipulates model behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  Malicious or compromised tools (MCP)
&lt;/h3&gt;

&lt;p&gt;An MCP server is code plus data exposed to the model. A hostile server can return misleading tool descriptions, exfiltrate arguments passed by the model, or perform actions beyond what the user intended when the host executes tool calls without scoped credentials.&lt;/p&gt;

&lt;h3&gt;
  
  
  Malicious or impersonated agents (A2A)
&lt;/h3&gt;

&lt;p&gt;An agent that accepts tasks may be evil, compromised, or simply over-permissioned. Agent Cards describe capabilities; they do not prove identity unless you verify signatures, TLS, and issuer trust.&lt;/p&gt;

&lt;h3&gt;
  
  
  Confused deputy
&lt;/h3&gt;

&lt;p&gt;Agent B holds permission to access a finance API. Agent A, with lower privilege, asks B to "summarize this invoice" while smuggling a transfer instruction in an artifact. B executes using its own credentials unless delegation scope is enforced end to end.&lt;/p&gt;

&lt;h3&gt;
  
  
  Over-broad permissions and hidden delegation chains
&lt;/h3&gt;

&lt;p&gt;User approves one step. The orchestrator silently chains three A2A hops and five MCP calls. The user never sees the full graph, but the organization is still accountable for the outcome.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prompt injection through artifacts and cross-agent messages
&lt;/h3&gt;

&lt;p&gt;Injection is not only a user-message problem. A PDF artifact, a web page fetched by a tool, or a message from Agent C can carry instructions aimed at Agent D's model. Treat &lt;strong&gt;all&lt;/strong&gt; protocol-carried content as untrusted input at the model boundary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Poisoned or misleading Agent Cards
&lt;/h3&gt;

&lt;p&gt;Descriptions and skill names are prompt surface area. A card that advertises &lt;code&gt;safe_read_only_analysis&lt;/code&gt; while accepting write-capable backends is a social-engineering layer, not a technical guarantee.&lt;/p&gt;

&lt;h2&gt;
  
  
  Identity Model for Multi-Agent Systems
&lt;/h2&gt;

&lt;p&gt;Protocol security begins with clear identity types and what each one is allowed to prove.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Identity type&lt;/th&gt;
&lt;th&gt;What it represents&lt;/th&gt;
&lt;th&gt;Typical proof&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Human user&lt;/td&gt;
&lt;td&gt;End user or operator who initiated work&lt;/td&gt;
&lt;td&gt;OIDC session, SSO token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent service&lt;/td&gt;
&lt;td&gt;Deployed agent runtime (orchestrator, specialist)&lt;/td&gt;
&lt;td&gt;OAuth client credentials, mTLS cert&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCP server&lt;/td&gt;
&lt;td&gt;Tool provider process&lt;/td&gt;
&lt;td&gt;API key, mTLS, scoped service account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Task / session&lt;/td&gt;
&lt;td&gt;Unit of work spanning hops&lt;/td&gt;
&lt;td&gt;task ID, trace ID, delegated scope token&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A2A's Agent Card advertises &lt;strong&gt;supported authentication schemes&lt;/strong&gt; (OAuth 2.0, API keys, mTLS, and similar patterns aligned with OpenAPI practice) and &lt;strong&gt;skills&lt;/strong&gt; with optional security requirements. The card is discovery metadata, not a trust anchor. Clients obtain credentials out of band and send them in standard HTTP headers on every request; servers must validate on every call and return 401 or 403 when auth or scope fails.&lt;/p&gt;

&lt;h3&gt;
  
  
  Internal vs external views of the same agent
&lt;/h3&gt;

&lt;p&gt;Production agents often publish a &lt;strong&gt;public&lt;/strong&gt; Agent Card with a limited skill list and a richer &lt;strong&gt;authenticated&lt;/strong&gt; card for internal callers. The A2A specification allows extended cards for authenticated clients. Use that split deliberately: partners should not see internal skills, and internal orchestrators should not rely on public discovery alone for authorization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authentication and Authorization for MCP and A2A
&lt;/h2&gt;

&lt;p&gt;Authentication answers &lt;strong&gt;who is calling&lt;/strong&gt;. Authorization answers &lt;strong&gt;what they may do&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  MCP tool access
&lt;/h3&gt;

&lt;p&gt;For each MCP connection, define:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which agent host may connect&lt;/li&gt;
&lt;li&gt;which tools are enabled for that host&lt;/li&gt;
&lt;li&gt;which OS user or service account executes side effects&lt;/li&gt;
&lt;li&gt;whether the human user must approve each mutating call&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Prefer &lt;strong&gt;tool allowlists&lt;/strong&gt; over "connect everything" MCP configs. A coding agent does not need payroll MCP servers on the same profile as a public support bot.&lt;/p&gt;

&lt;h3&gt;
  
  
  A2A agent access
&lt;/h3&gt;

&lt;p&gt;For each agent peer relationship, define:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which caller agent IDs may invoke which skills&lt;/li&gt;
&lt;li&gt;maximum delegation depth&lt;/li&gt;
&lt;li&gt;which artifact types may cross the boundary&lt;/li&gt;
&lt;li&gt;whether user context must propagate as signed claims&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Map OAuth scopes (or equivalent) to &lt;strong&gt;skills&lt;/strong&gt;, not to blanket agent admin. Least privilege at the token layer beats hope at the prompt layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gateway-enforced vs per-agent policy
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Per-agent policy&lt;/strong&gt; works when one team owns the whole graph and releases are coordinated. &lt;strong&gt;Gateway-enforced policy&lt;/strong&gt; works when multiple teams, tenants, or vendors share an agent network and you need one place to enforce allowlists, rate limits, and audit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
    U[User / client] --&amp;gt; G[A2A gateway]
    G --&amp;gt; O[Orchestrator agent]
    O --&amp;gt;|A2A scoped token| S1[Specialist agent]
    O --&amp;gt;|A2A scoped token| S2[Specialist agent]
    S1 --&amp;gt; MG[MCP gateway]
    S2 --&amp;gt; MG
    MG --&amp;gt; T1[MCP tool servers]
    MG --&amp;gt; T2[MCP tool servers]
    G --&amp;gt; A[Audit log]
    MG --&amp;gt; A
    S1 --&amp;gt; A
    S2 --&amp;gt; A
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A2A Gateway as the Control Plane
&lt;/h2&gt;

&lt;p&gt;An A2A gateway is not strictly required by the protocol, but it becomes necessary when agent traffic needs centralized governance.&lt;/p&gt;

&lt;p&gt;A gateway typically handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;authentication termination and token exchange&lt;/li&gt;
&lt;li&gt;routing to the correct agent service by skill or tenant&lt;/li&gt;
&lt;li&gt;policy checks before tasks are accepted or forwarded&lt;/li&gt;
&lt;li&gt;protocol version negotiation&lt;/li&gt;
&lt;li&gt;rate limiting and abuse detection&lt;/li&gt;
&lt;li&gt;structured audit emission on every task transition&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  When a gateway is overkill vs necessary
&lt;/h3&gt;

&lt;p&gt;A gateway is often overkill for a single orchestrator and two specialist agents in one Kubernetes namespace maintained by one team. It becomes necessary when partners invoke your agents, when multiple business units share infrastructure, when compliance requires uniform logging, or when you cannot trust every agent implementation to enforce policy correctly.&lt;/p&gt;

&lt;p&gt;Pair an &lt;strong&gt;A2A gateway&lt;/strong&gt; with an &lt;strong&gt;MCP gateway&lt;/strong&gt; (or MCP proxy) so tool access receives the same treatment: identity, allowlists, egress controls, and audit at the tool boundary rather than only at the chat UI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Partner-facing vs internal Agent Cards
&lt;/h3&gt;

&lt;p&gt;Publish different discovery metadata for external and internal callers. External cards expose narrow skills and stricter auth. Internal cards may list maintenance or admin skills but must never be reachable without stronger authentication than the public card implies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agent Registry and Discovery Security
&lt;/h2&gt;

&lt;p&gt;Discovery is part of the attack surface. Anyone who controls what agents appear "available" controls where orchestrators send work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Registry vs well-known Agent Card URLs
&lt;/h3&gt;

&lt;p&gt;Small deployments use well-known URLs per agent (&lt;code&gt;/.well-known/agent-card.json&lt;/code&gt;). Enterprise deployments add a &lt;strong&gt;registry&lt;/strong&gt; that indexes agent IDs, versions, endpoints, owners, and policy tags. The registry is a policy object: entries should record which tenants may discover which agents, not only where they live.&lt;/p&gt;

&lt;h3&gt;
  
  
  Versioning, deprecation, and ownership
&lt;/h3&gt;

&lt;p&gt;Registry records need owners, change history, and deprecation dates. An orchestrator that caches Agent Cards must refresh on TTL and verify signatures where supported. Stale cards are how retired skills keep receiving traffic long after a vulnerability is patched.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enterprise internal networks vs external partners
&lt;/h3&gt;

&lt;p&gt;Internal agent meshes can rely on mTLS and private DNS. Partner agents need explicit federation rules, contractually scoped skills, and stronger artifact inspection because you do not control their runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Delegation Across Agent Boundaries
&lt;/h2&gt;

&lt;p&gt;Delegation is where A2A security is won or lost. When Agent A sends a task to Agent B, three questions must have crisp answers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Whose authority is being exercised?&lt;/strong&gt; The user's, A's service account, or a blended delegated token?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What is B allowed to do with that authority?&lt;/strong&gt; Read-only analysis, or mutating tools on A's behalf?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who is accountable if B exceeds scope?&lt;/strong&gt; A, B, the gateway policy, or the human who approved an unclear prompt?&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Propagating user intent vs over-delegation
&lt;/h3&gt;

&lt;p&gt;Pass &lt;strong&gt;signed delegation claims&lt;/strong&gt; that include user ID, original task ID, allowed skills, expiry, and maximum hop count. Downstream agents must reject tasks that expand scope silently. If B needs higher privilege than A held, transition to &lt;code&gt;input_required&lt;/code&gt; and obtain explicit human approval rather than upgrading tokens invisibly.&lt;/p&gt;

&lt;p&gt;Human-in-the-loop approval flows for risky delegation are covered in &lt;a href="https://www.glukhov.org/ai-systems/architecture/a2a-streaming-async-task-lifecycle/" rel="noopener noreferrer"&gt;A2A Streaming and Async Tasks for Long-Running Agent Workflows&lt;/a&gt; where &lt;code&gt;input_required&lt;/code&gt; is a first-class task state rather than an error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sequenceDiagram
    participant User
    participant Orch as Orchestrator agent
    participant GW as A2A gateway
    participant Spec as Specialist agent
    participant MCP as MCP tool server
    User-&amp;gt;&amp;gt;Orch: Request with user token
    Orch-&amp;gt;&amp;gt;GW: Delegate task (scoped delegation token)
    GW-&amp;gt;&amp;gt;GW: Policy check scope + hop count
    GW-&amp;gt;&amp;gt;Spec: Forward task (reduced scope token)
    Spec-&amp;gt;&amp;gt;MCP: Tool call (tool-scoped credential)
    MCP-&amp;gt;&amp;gt;MCP: Enforce allowlist + user context
    Spec--&amp;gt;&amp;gt;GW: Artifact + audit events
    GW--&amp;gt;&amp;gt;Orch: Task update
    Orch--&amp;gt;&amp;gt;User: Final response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Separate reasoning from execution permissions
&lt;/h3&gt;

&lt;p&gt;An agent may need broad &lt;strong&gt;read&lt;/strong&gt; access to plan while &lt;strong&gt;write&lt;/strong&gt; tools sit behind approval. Split credentials or use distinct MCP profiles for planning vs execution so a model mistake cannot immediately mutate production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Audit Trails and Answer Provenance
&lt;/h2&gt;

&lt;p&gt;If you cannot reconstruct a delegation chain, you cannot explain an incident, pass an audit, or dispute a billing anomaly.&lt;/p&gt;

&lt;p&gt;Log at three layers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gateway:&lt;/strong&gt; authentication result, policy decision, routed agent ID, task ID, parent task ID, rate-limit events.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent:&lt;/strong&gt; task state transitions, messages sent/received, model/tool invocations (arguments redacted as needed), artifacts created, delegation outward.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP server:&lt;/strong&gt; tool name, caller agent ID, user context, success/failure, latency, rows affected or resource IDs (policy permitting).&lt;/p&gt;

&lt;p&gt;Correlate with &lt;strong&gt;trace ID&lt;/strong&gt; across all layers. &lt;a href="https://www.glukhov.org/observability/observability-for-llm-systems/" rel="noopener noreferrer"&gt;Observability for LLM Systems&lt;/a&gt; covers instrumentation backends; this article defines &lt;strong&gt;what&lt;/strong&gt; must be captured so those backends have meaningful signal.&lt;/p&gt;

&lt;p&gt;Final answer provenance should answer: which user, which orchestrator task, which specialist agents, which tools, which artifacts influenced the text the user saw, and which policy gates fired along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Runtime Policy, Egress, and Secrets
&lt;/h2&gt;

&lt;p&gt;Runtime policy engines (OPA, Cedar, custom rule services) evaluate structured events: "tool X with args Y for user Z." They complement guardrails because they do not depend on the model behaving well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Human approval&lt;/strong&gt; belongs in runtime policy for irreversible or high-cost actions: payments, external email, production config changes, privilege grants.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Egress controls&lt;/strong&gt; limit which domains MCP servers and agents may call. An agent that can both read secrets and POST to arbitrary URLs is a data-loss waiting to happen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Secrets&lt;/strong&gt; never belong in Agent Cards or prompts. MCP hosts should inject short-lived credentials at execution time from a secrets manager. For transport encryption, key management, and baseline infra security patterns, see &lt;a href="https://www.glukhov.org/app-architecture/security/securing-data-at-rest-in-transit-runtime/" rel="noopener noreferrer"&gt;Architectural Patterns for Securing Data&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Push notification webhooks in async A2A flows need the same rigor: verify sender identity, reject stale events, and never treat a webhook payload as authorization on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reference Security Architecture
&lt;/h2&gt;

&lt;p&gt;The following diagram summarizes a production-oriented layout for &lt;a href="https://www.glukhov.org/ai-systems/mcp/a2a-vs-mcp-ai-agent-protocols/" rel="noopener noreferrer"&gt;A2A outside, MCP inside&lt;/a&gt; deployments at scale.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TB
    subgraph Client layer
        U[User / API client]
    end
    subgraph Control plane
        GW[A2A gateway]
        REG[Agent registry]
        POL[Policy engine]
        AUD[Audit log]
        SEC[Secrets manager]
    end
    subgraph Agent layer
        OR[Orchestrator]
        SA[Specialist agents]
    end
    subgraph Tool layer
        MG[MCP gateway]
        MCP[MCP servers]
    end
    subgraph Observability
        OBS[Tracing + metrics]
    end
    U --&amp;gt; GW
    GW --&amp;gt; REG
    GW --&amp;gt; POL
    GW --&amp;gt; OR
    OR --&amp;gt; GW
    GW --&amp;gt; SA
    SA --&amp;gt; MG
    MG --&amp;gt; MCP
    POL --&amp;gt; GW
    POL --&amp;gt; MG
    SEC --&amp;gt; SA
    SEC --&amp;gt; MCP
    GW --&amp;gt; AUD
    MG --&amp;gt; AUD
    SA --&amp;gt; AUD
    AUD --&amp;gt; OBS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The orchestrator sees specialist agents through A2A. Specialists see tools through MCP. Users never receive raw MCP credentials, and partners never receive internal skill surfaces without policy review.&lt;/p&gt;

&lt;p&gt;For protocol concepts (Agent Cards, tasks, artifacts), see &lt;a href="https://www.glukhov.org/ai-systems/architecture/a2a-protocol-explained/" rel="noopener noreferrer"&gt;What Is the A2A Protocol?&lt;/a&gt;. For adoption and enterprise framing, see &lt;a href="https://www.glukhov.org/ai-systems/comparisons/a2a-protocol-2026-adoption/" rel="noopener noreferrer"&gt;Google A2A Protocol in 2026&lt;/a&gt;. For topology when many agents coordinate, see &lt;a href="https://www.glukhov.org/ai-systems/architecture/multi-agent-orchestration-patterns/" rel="noopener noreferrer"&gt;Multi-Agent Orchestration Patterns&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production Checklist for A2A and MCP Security
&lt;/h2&gt;

&lt;p&gt;Before exposing agent protocols beyond a trusted sandbox, verify:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Identity and auth&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] No anonymous agents in production paths&lt;/li&gt;
&lt;li&gt;[ ] Every MCP and A2A call authenticated on every request&lt;/li&gt;
&lt;li&gt;[ ] OAuth scopes or equivalent mapped to skills/tools, not blanket admin&lt;/li&gt;
&lt;li&gt;[ ] Public vs authenticated Agent Card views defined intentionally&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Delegation and policy&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Delegation tokens carry user ID, task ID, scope, expiry, hop limit&lt;/li&gt;
&lt;li&gt;[ ] Downstream agents reject scope expansion without explicit approval&lt;/li&gt;
&lt;li&gt;[ ] High-risk tools require runtime policy or human approval&lt;/li&gt;
&lt;li&gt;[ ] Reasoning and execution use separate credentials where possible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Discovery and registry&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Agent registry entries have owners and version history&lt;/li&gt;
&lt;li&gt;[ ] Agent Cards refreshed on TTL; signatures verified where supported&lt;/li&gt;
&lt;li&gt;[ ] Partner agents federated with explicit skill allowlists&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Audit and observability&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Gateway, agent, and MCP layers emit correlated audit events&lt;/li&gt;
&lt;li&gt;[ ] Delegation chains logged with parent and child task IDs&lt;/li&gt;
&lt;li&gt;[ ] Artifact provenance recorded for final answers&lt;/li&gt;
&lt;li&gt;[ ] Trace IDs connect to observability backends&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Abuse and resilience&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Rate limits per user, agent, and tenant&lt;/li&gt;
&lt;li&gt;[ ] Timeout policies on delegated tasks&lt;/li&gt;
&lt;li&gt;[ ] Egress allowlists on tool servers&lt;/li&gt;
&lt;li&gt;[ ] Secrets in a manager, not in cards, prompts, or repos&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;A2A and MCP interoperability is powerful because agents and tools can compose across team and vendor boundaries, but that power is unsafe without identity, authorization, delegation limits, and audit design. Guardrails protect the model conversation; protocol security protects the actions agents take on behalf of users.&lt;/p&gt;

&lt;p&gt;Treat Agent Cards as advertisements, delegation as a signed contract, MCP tools as privileged code execution, and audit logs as the evidence chain you will need when something interesting happens at 2 a.m.&lt;/p&gt;

&lt;p&gt;Build the gateway when governance needs a single throat to choke. Split credentials before you split agents. Log every hop so the answer "the model decided" is never the final incident report.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between LLM guardrails and A2A MCP agent security?&lt;/strong&gt;&lt;br&gt;
Guardrails constrain model input and output. Protocol security constrains who may invoke tools, delegate tasks, and act on whose behalf across MCP and A2A with identity, authorization, and audit trails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How should agent identity work in an A2A deployment?&lt;/strong&gt;&lt;br&gt;
Separate human, agent service, and task identities. Validate credentials on every request, use scoped tokens, and treat Agent Cards as discovery metadata rather than proof of trust.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the confused deputy problem in multi-agent systems?&lt;/strong&gt;&lt;br&gt;
It occurs when a privileged agent or tool performs a sensitive action because a less privileged caller smuggled instructions through delegation or artifacts. Enforce scope at every hop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do you need an A2A gateway in production?&lt;/strong&gt;&lt;br&gt;
Single-team internal deployments may enforce policy per agent. Multi-tenant, multi-vendor, or partner-facing networks usually need a gateway for centralized auth, routing, rate limits, and audit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What should an A2A MCP audit log contain?&lt;/strong&gt;&lt;br&gt;
User ID, agent ID, task ID, parent task ID, tool calls, policy decisions, artifacts, and timestamps correlated with trace IDs across gateway, agent, and MCP layers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A2A Protocol -- Enterprise-ready security topics: &lt;a href="https://github.com/a2aproject/A2A/blob/main/docs/topics/enterprise-ready.md" rel="noopener noreferrer"&gt;https://github.com/a2aproject/A2A/blob/main/docs/topics/enterprise-ready.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;A2A Protocol -- Specification overview: &lt;a href="https://a2a-protocol.org/latest/specification/" rel="noopener noreferrer"&gt;https://a2a-protocol.org/latest/specification/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;A2A Protocol -- Streaming and push notification security: &lt;a href="https://a2a-protocol.org/latest/topics/streaming-and-async/" rel="noopener noreferrer"&gt;https://a2a-protocol.org/latest/topics/streaming-and-async/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>architecture</category>
      <category>security</category>
    </item>
    <item>
      <title>A2A Streaming and Async Tasks for Long-Running Agent Workflows</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Fri, 10 Jul 2026 09:36:29 +0000</pubDate>
      <link>https://dev.to/rosgluk/a2a-streaming-and-async-tasks-for-long-running-agent-workflows-1193</link>
      <guid>https://dev.to/rosgluk/a2a-streaming-and-async-tasks-for-long-running-agent-workflows-1193</guid>
      <description>&lt;p&gt;Most AI agent demos still behave like chat completions with extra steps: you send a prompt, wait a few seconds, and get an answer back in one response.&lt;/p&gt;

&lt;p&gt;Real agent work often does not fit that pattern. Research, code review, procurement analysis, incident investigation, and multi-step planning can run for minutes or hours, and they may need clarification halfway through, stream partial results, delegate to another agent, and produce files rather than a single text reply. That is where the A2A protocol's async model matters within the broader &lt;a href="https://www.glukhov.org/ai-systems/" rel="noopener noreferrer"&gt;AI Systems&lt;/a&gt; cluster, because A2A treats long-running work as a &lt;strong&gt;Task&lt;/strong&gt; with a lifecycle instead of a one-shot HTTP response. Clients can stay connected via Server-Sent Events (SSE), poll task state, or register push webhooks when they cannot hold a connection open.&lt;/p&gt;

&lt;p&gt;This article covers operational design for those workflows, including when to stream versus poll versus push, how &lt;code&gt;input_required&lt;/code&gt; fits human-in-the-loop flows, failure handling, and what to instrument in production. For Agent Cards, messages, parts, and the full task model, see &lt;a href="https://www.glukhov.org/ai-systems/architecture/a2a-protocol-explained/" rel="noopener noreferrer"&gt;What Is the A2A Protocol? Agent Cards and Tasks Explained&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Long-Running A2A Agent Tasks Need Async Design
&lt;/h2&gt;

&lt;p&gt;A synchronous request/response mental model breaks down quickly once agent work spans tools, delegation, approvals, and large artifacts. An agent task may call multiple MCP servers internally, delegate sub-work to another agent over A2A, wait for human approval, generate large artifacts in chunks, fail partway through and need partial recovery, and accumulate token cost across several hops. HTTP APIs can approximate this with timeouts, background jobs, and ad hoc status endpoints, but A2A bakes task identity and state into the protocol so clients and gateways can reason about work consistently. For how those layers fit inside a production assistant before you add async A2A boundaries, see &lt;a href="https://www.glukhov.org/ai-systems/architecture/ai-assistant-architecture/" rel="noopener noreferrer"&gt;AI Assistant Architecture: LLM, Memory, Tools, Routing, Observability&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;My bias is practical: &lt;strong&gt;do not create a Task for everything&lt;/strong&gt;, because a one-line summary does not need a lifecycle. Use a Task when work is stateful, auditable, long-running, artifact-producing, or may need input mid-flight. The rule of thumb from the explainer still holds: simple interactions can return a Message, while complex work should return a Task.&lt;/p&gt;

&lt;h2&gt;
  
  
  A2A Task Lifecycle and State Transitions
&lt;/h2&gt;

&lt;p&gt;An A2A Task moves through states that clients can query at any time. Exact naming varies slightly by implementation, but the model is stable across servers that follow the protocol.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;stateDiagram-v2
    [*] --&amp;gt; submitted
    submitted --&amp;gt; working
    working --&amp;gt; input_required
    input_required --&amp;gt; working
    working --&amp;gt; completed
    working --&amp;gt; failed
    working --&amp;gt; canceled
    working --&amp;gt; rejected
    submitted --&amp;gt; rejected
    input_required --&amp;gt; failed
    input_required --&amp;gt; canceled
    completed --&amp;gt; [*]
    failed --&amp;gt; [*]
    canceled --&amp;gt; [*]
    rejected --&amp;gt; [*]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;submitted&lt;/strong&gt; state means the client sent work and the agent accepted or queued it. In &lt;strong&gt;working&lt;/strong&gt;, the agent is actively processing, which may include tool calls, delegation, or streaming partial output. The &lt;strong&gt;input_required&lt;/strong&gt; state indicates the agent paused because it needs more input, clarification, or human approval, and it is not a failure state. &lt;strong&gt;completed&lt;/strong&gt; is terminal success with artifacts available; &lt;strong&gt;failed&lt;/strong&gt; is a terminal error whose details and partial artifacts depend on implementation; &lt;strong&gt;canceled&lt;/strong&gt; means a client, gateway, or authorized caller stopped the task; and &lt;strong&gt;rejected&lt;/strong&gt; means the agent refused the task because of policy, capability mismatch, or auth.&lt;/p&gt;

&lt;h3&gt;
  
  
  When input_required pauses versus fails a workflow
&lt;/h3&gt;

&lt;p&gt;Treat &lt;code&gt;input_required&lt;/code&gt; as a deliberate &lt;strong&gt;pause&lt;/strong&gt;, not an exception. The agent is telling you it cannot proceed without something from you, whether that is a missing parameter, a policy confirmation, or a manager sign-off on a high-risk action. A workflow &lt;strong&gt;fails&lt;/strong&gt; when the task reaches &lt;code&gt;failed&lt;/code&gt; or &lt;code&gt;rejected&lt;/code&gt;, or when a caller exceeds a timeout waiting for input that never arrives, so you should design explicit timeouts for human steps rather than letting approvals sit indefinitely.&lt;/p&gt;

&lt;p&gt;An approval that waits three days without escalation is a stuck workflow, not a patient one, and stuck workflows clog task stores while making observability dashboards harder to read.&lt;/p&gt;

&lt;h3&gt;
  
  
  Who can cancel an A2A task
&lt;/h3&gt;

&lt;p&gt;Cancellation authority should be defined at design time rather than debated during an incident. The &lt;strong&gt;client&lt;/strong&gt; usually can cancel tasks it created; a &lt;strong&gt;gateway&lt;/strong&gt; may cancel on behalf of tenants, policy violations, or budget limits; and an &lt;strong&gt;upstream agent&lt;/strong&gt; may cancel delegated work when orchestrating over A2A if the protocol and policy allow it. Log who canceled and why, because in multi-agent chains orphan work is a common source of surprise token bills.&lt;/p&gt;

&lt;h2&gt;
  
  
  Human-in-the-Loop with input_required Task States
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;input_required&lt;/code&gt; is one of A2A's most underused design features, and many teams treat it as an error code when it is actually a first-class workflow state. In production you will hit cases where the agent &lt;strong&gt;should&lt;/strong&gt; stop, such as spending budget on an ambiguous request, executing an irreversible action, accessing sensitive data without scope confirmation, or delegating to a specialist that needs explicit user intent. Model these as deliberate transitions to &lt;code&gt;input_required&lt;/code&gt;, with a clear message explaining what is needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Approval flows for risky A2A delegation
&lt;/h3&gt;

&lt;p&gt;When Agent A delegates to Agent B over A2A and Agent B enters &lt;code&gt;input_required&lt;/code&gt; for human approval, three systems need to agree on what happens next. The downstream agent pauses and exposes what it needs, the orchestrator or gateway surfaces that pause to the user, and the user's response resumes the task via a new message. The &lt;a href="https://www.glukhov.org/ai-systems/mcp/a2a-vs-mcp-ai-agent-protocols/" rel="noopener noreferrer"&gt;A2A vs MCP&lt;/a&gt; comparison explains why delegation across agent boundaries is a different problem from tool access, and why approval semantics belong at the task layer rather than inside a single MCP call. Do not silently auto-approve because the UX is inconvenient, since expensive mistakes usually come from convenience shortcuts rather than from missing models.&lt;/p&gt;

&lt;h3&gt;
  
  
  UX patterns for paused A2A tasks
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Blocking wait&lt;/strong&gt; means the UI shows a spinner or approval card until the task leaves &lt;code&gt;input_required&lt;/code&gt;, which works well for short human steps. &lt;strong&gt;Non-blocking wait&lt;/strong&gt; means the client records the task ID, lets the user continue elsewhere, and uses polling or push to notify when input is needed again, which is required for mobile, email-linked approvals, or multi-tab assistants. &lt;strong&gt;Timeout when humans are slow&lt;/strong&gt; means defining an SLA per step and, after N hours, transitioning to &lt;code&gt;failed&lt;/code&gt; or escalating to another queue, because unbounded waits clog task stores and confuse observability dashboards.&lt;/p&gt;

&lt;h3&gt;
  
  
  How an A2A gateway handles input_required
&lt;/h3&gt;

&lt;p&gt;If you run an A2A gateway, decide whether it forwards &lt;code&gt;input_required&lt;/code&gt; events transparently, aggregates pauses from multiple downstream agents into one user prompt, or enforces that certain skills always require approval before leaving &lt;code&gt;input_required&lt;/code&gt;. Auth and policy for approved actions belong in a dedicated security article; for now, assume every resumed task should carry the same user identity and scope as the original request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing Sync, SSE Streaming, Polling, or Push Notifications
&lt;/h2&gt;

&lt;p&gt;A2A supports multiple interaction modes, and the right choice depends on client capabilities and latency needs rather than on which mode sounds most modern.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Client requirements&lt;/th&gt;
&lt;th&gt;Tradeoffs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sync (SendMessage, short Task)&lt;/td&gt;
&lt;td&gt;Quick work, immediate Messages&lt;/td&gt;
&lt;td&gt;Simple HTTP client&lt;/td&gt;
&lt;td&gt;Timeouts on slow agents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSE streaming&lt;/td&gt;
&lt;td&gt;Live progress, incremental artifacts&lt;/td&gt;
&lt;td&gt;Long-lived connection&lt;/td&gt;
&lt;td&gt;Proxies, mobile background limits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Polling (GetTask)&lt;/td&gt;
&lt;td&gt;Batch clients, simple integrations&lt;/td&gt;
&lt;td&gt;Timer + task ID&lt;/td&gt;
&lt;td&gt;Higher latency, more requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Push webhooks&lt;/td&gt;
&lt;td&gt;Mobile, serverless, multi-hour jobs&lt;/td&gt;
&lt;td&gt;HTTPS receiver + verification&lt;/td&gt;
&lt;td&gt;Async complexity, security hardening&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Read Agent Card capability flags first
&lt;/h3&gt;

&lt;p&gt;Before choosing a mode, read the agent's &lt;strong&gt;Agent Card&lt;/strong&gt;, because streaming requires &lt;code&gt;capabilities.streaming: true&lt;/code&gt; and push notification support is advertised separately. Clients that assume every agent streams will break against minimal implementations, so negotiation is not ceremonial: it prevents runtime failures when a specialist agent only supports poll-based status checks.&lt;/p&gt;

&lt;h3&gt;
  
  
  When to use assistant-side polling around A2A
&lt;/h3&gt;

&lt;p&gt;Your assistant runtime may wrap A2A task polling in a scheduler loop rather than exposing raw protocol details to the user. That pattern overlaps with general &lt;strong&gt;polling agents&lt;/strong&gt;, which are background processes that wake up, check state, and act. For durable scheduling, idempotency, and queue patterns outside A2A specifically, see &lt;a href="https://www.glukhov.org/ai-systems/architecture/polling-agents-ai-assistants-implementation-patterns/" rel="noopener noreferrer"&gt;Polling Agents in AI Assistants: 11 Implementation Patterns&lt;/a&gt;. Use assistant polling when you orchestrate many A2A tasks from a single control plane, and use native A2A streaming or push when the client connects directly to the agent boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  A2A Server-Sent Events (SSE) Streaming
&lt;/h2&gt;

&lt;p&gt;SSE is A2A's primary real-time channel. The client calls &lt;strong&gt;SendStreamingMessage&lt;/strong&gt;, opens an HTTP connection, and receives a &lt;code&gt;text/event-stream&lt;/code&gt; response until the task reaches a terminal or interrupted state. Each event's payload is JSON-RPC-shaped, and typical result types include a &lt;strong&gt;Task&lt;/strong&gt; snapshot, a &lt;strong&gt;TaskStatusUpdateEvent&lt;/strong&gt; for lifecycle transitions and intermediate agent messages, and a &lt;strong&gt;TaskArtifactUpdateEvent&lt;/strong&gt; for chunked artifact delivery with &lt;code&gt;append&lt;/code&gt; and &lt;code&gt;lastChunk&lt;/code&gt; hints for reassembly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sequenceDiagram
    participant Client
    participant A2A Server
    Client-&amp;gt;&amp;gt;A2A Server: SendStreamingMessage
    A2A Server--&amp;gt;&amp;gt;Client: HTTP 200 text/event-stream
    loop Until terminal or input_required
        A2A Server--&amp;gt;&amp;gt;Client: TaskStatusUpdateEvent
        A2A Server--&amp;gt;&amp;gt;Client: TaskArtifactUpdateEvent (optional)
    end
    A2A Server--&amp;gt;&amp;gt;Client: Close stream
    Note over Client,A2A Server: On disconnect before terminal state,&amp;lt;br/&amp;gt;client may call SubscribeToTask
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Streaming progress updates and partial artifacts
&lt;/h3&gt;

&lt;p&gt;Streaming shines when users should &lt;strong&gt;see work happening&lt;/strong&gt;, whether that means step counters ("3 of 7 sources reviewed"), partial text generation, incremental file chunks for large reports, or state transitions from &lt;code&gt;working&lt;/code&gt; to &lt;code&gt;input_required&lt;/code&gt; without polling. Design UI around event types rather than around a single final blob, because if you only display output when &lt;code&gt;completed&lt;/code&gt; arrives you might as well poll.&lt;/p&gt;

&lt;h3&gt;
  
  
  SSE connection drops and resubscription
&lt;/h3&gt;

&lt;p&gt;Networks drop, laptops sleep, and load balancers idle-timeout SSE connections, so long streams need recovery logic rather than optimistic assumptions. A2A provides &lt;strong&gt;SubscribeToTask&lt;/strong&gt; so clients can reconnect to an in-progress task stream, and your client SDK should persist &lt;code&gt;taskId&lt;/code&gt; locally, detect stream closure before terminal state, resubscribe with backoff, and de-duplicate events if the server replays overlapping state. Without resubscription logic, long tasks feel fragile in production even when the agent backend is healthy.&lt;/p&gt;

&lt;h2&gt;
  
  
  A2A Push Notifications and Webhooks
&lt;/h2&gt;

&lt;p&gt;Push fits scenarios where SSE is a poor match, such as mobile apps in the background, serverless handlers, or tasks that run for hours or days. The client supplies a &lt;strong&gt;PushNotificationConfig&lt;/strong&gt; with a &lt;code&gt;url&lt;/code&gt; (HTTPS webhook on the client side), an optional &lt;code&gt;token&lt;/code&gt; for validating incoming POSTs, and optional &lt;code&gt;authentication&lt;/code&gt; details for how the A2A server authenticates to the webhook. Configuration can ride along with the initial SendMessage or SendStreamingMessage call, or be added later via &lt;strong&gt;CreateTaskPushNotificationConfig&lt;/strong&gt; for an existing task.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sequenceDiagram
    participant Client
    participant A2A Server
    participant Webhook
    Client-&amp;gt;&amp;gt;A2A Server: SendMessage + PushNotificationConfig
    A2A Server--&amp;gt;&amp;gt;Client: taskId
    Note over A2A Server: Task runs asynchronously
    A2A Server-&amp;gt;&amp;gt;Webhook: POST state change notification
    Webhook-&amp;gt;&amp;gt;A2A Server: GetTask(taskId)
    A2A Server--&amp;gt;&amp;gt;Webhook: Updated Task + artifacts
    Webhook-&amp;gt;&amp;gt;Client: Resume workflow / notify user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a significant update occurs, the A2A server POSTs to the webhook and the client typically calls &lt;strong&gt;GetTask&lt;/strong&gt; with the notified &lt;code&gt;taskId&lt;/code&gt; to fetch the full updated Task and artifacts. Push is a &lt;strong&gt;signal&lt;/strong&gt;, not a full payload transport.&lt;/p&gt;

&lt;h3&gt;
  
  
  When push beats an open SSE connection
&lt;/h3&gt;

&lt;p&gt;Prefer push when the client cannot maintain SSE (mobile, edge functions), when updates are infrequent and milestone-based rather than token-by-token, or when you want the server to wake a disconnected workflow engine. Prefer SSE when users watch progress live, when artifacts stream in many small chunks, or when latency below a few seconds matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Correlating push notifications to A2A tasks
&lt;/h3&gt;

&lt;p&gt;Every push handler should log and propagate the &lt;code&gt;taskId&lt;/code&gt;, a trace or correlation ID from the original request, the event type or state transition, and a timestamp from the notification so stale events can be rejected. Replay attacks and duplicate deliveries happen in production, so idempotent handlers are not optional.&lt;/p&gt;

&lt;h3&gt;
  
  
  Push endpoint security overview
&lt;/h3&gt;

&lt;p&gt;Push introduces SSRF risk on the server when malicious clients register internal URLs, and impersonation risk on the client when fake POSTs arrive at the webhook. Mitigations include URL allowlists, ownership verification, signed JWTs with JWKS, timestamp checks, and validating the config token. The full threat model, identity layers, and gateway controls live in &lt;a href="https://www.glukhov.org/llm-architecture/guardrails/a2a-mcp-agent-security/" rel="noopener noreferrer"&gt;A2A and MCP Agent Security: Identity, Delegation, and Audit Trails&lt;/a&gt;; until you have read it, treat webhook verification with the same seriousness as payment callbacks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Async A2A Workflow Patterns
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Fire-and-follow task submission
&lt;/h3&gt;

&lt;p&gt;The client submits a task, receives a task ID immediately, and disconnects, then later polls GetTask or waits for push. This is the default pattern for serverless and batch pipelines, but you should persist the task ID in durable storage before acknowledging the user, because serverless invocations that forget the ID lose the work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resuming a task after input_required
&lt;/h3&gt;

&lt;p&gt;After &lt;code&gt;input_required&lt;/code&gt;, the user sends a new message against the same task and the agent transitions back to &lt;code&gt;working&lt;/code&gt;. Design messages so resumption context is explicit, because "Approved: proceed with vendor X" beats a bare "yes" when you need to audit what was approved six hours later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Chained A2A delegation with intermediate artifacts
&lt;/h3&gt;

&lt;p&gt;Consider a research workflow where an orchestrator owns Task T1 and delegates retrieval, summarization, and verification to specialist agents, each with its own task ID and artifacts along the way.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    U[User] --&amp;gt; O[Orchestrator Task T1]
    O --&amp;gt;|A2A| R[Retrieval agent T2]
    R --&amp;gt; A2[artifact: raw sources]
    O --&amp;gt;|A2A| S[Summarization agent T3]
    S --&amp;gt; A3[artifact: draft summary]
    O --&amp;gt;|A2A| V[Verification agent T4]
    V --&amp;gt; A4[artifact: fact-check report]
    O --&amp;gt; F[final artifact: recommendation memo]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each hop has its own task ID and state machine, so the orchestrator should stream or poll downstream tasks independently, persist intermediate artifacts before starting the next hop, and fail gracefully if T3 completes but T4 rejects the draft. &lt;a href="https://www.glukhov.org/ai-systems/architecture/multi-agent-orchestration-patterns/" rel="noopener noreferrer"&gt;Multi-Agent Orchestration Patterns&lt;/a&gt; covers topology choice when those specialists run as separate services rather than in one runtime. Partial progress is valuable, and a failed verification should not delete a usable draft without a clear reason.&lt;/p&gt;

&lt;h3&gt;
  
  
  Durable task storage for delayed completion
&lt;/h3&gt;

&lt;p&gt;Task state and artifacts should survive process restarts. If your agent runs in Kubernetes, assume pods die mid-task and back task records and artifact blobs to a store the agent container does not own exclusively.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure Handling for Long-Running A2A Workflows
&lt;/h2&gt;

&lt;p&gt;Long-running workflows fail in predictable ways through timeouts, retries, partial artifacts, and unsafe cancellation, and each needs an explicit policy rather than ad hoc handling in client code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Per-hop and end-to-end timeout budgets
&lt;/h3&gt;

&lt;p&gt;Set timeouts at two levels: a &lt;strong&gt;per-hop&lt;/strong&gt; maximum for one agent task before escalation or cancel, and an &lt;strong&gt;end-to-end&lt;/strong&gt; maximum for the user-visible workflow. A retrieval agent that hangs should not block the entire orchestrator until the user's browser times out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Retries and idempotency for A2A tasks
&lt;/h3&gt;

&lt;p&gt;Retries without idempotency duplicate side effects such as double charges, duplicate tickets, and repeated emails. Use stable client message IDs or idempotency keys where the protocol allows, and for business mutations align with &lt;a href="https://www.glukhov.org/app-architecture/integration-patterns/idempotency-in-distributed-systems/" rel="noopener noreferrer"&gt;Idempotency in Distributed Systems That Actually Works&lt;/a&gt;. Retry only &lt;strong&gt;transient&lt;/strong&gt; failures like network blips or 503s, and do not retry &lt;code&gt;rejected&lt;/code&gt; or policy failures blindly because you will amplify cost and annoy downstream agents.&lt;/p&gt;

&lt;h3&gt;
  
  
  Partial artifact recovery policies
&lt;/h3&gt;

&lt;p&gt;When a task fails after producing partial artifacts, define whether you expose partial output to the user with a clear "incomplete" label, allow resume from the last good checkpoint, or discard partial output when it could mislead in medical, legal, or financial contexts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Safe cancellation across delegation chains
&lt;/h3&gt;

&lt;p&gt;Cancel downstream tasks when an upstream user aborts, use a delegation graph so cancel propagates, and log canceled tasks that already incurred cost because finance teams notice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observability for Async A2A Workflows
&lt;/h2&gt;

&lt;p&gt;You cannot debug multi-agent async work unless you can trace it across boundaries, which means correlating identifiers on every hop rather than relying on unstructured logs. Minimum correlation fields include a &lt;strong&gt;trace ID&lt;/strong&gt; per user-initiated workflow, a &lt;strong&gt;task ID&lt;/strong&gt; per agent task including delegated children, an &lt;strong&gt;agent ID&lt;/strong&gt; for the Agent Card or service that handled the hop, and a &lt;strong&gt;parent task ID&lt;/strong&gt; that links delegation chains.&lt;/p&gt;

&lt;p&gt;Log every state transition with timestamps, and log artifact creation events with size and hash rather than necessarily full content when PII policies apply. Attribute &lt;strong&gt;cost and latency per hop&lt;/strong&gt;, because multi-agent workflows hide token spend until the bill arrives and per-task cost labels make "which specialist is expensive?" answerable. For metrics, tracing backends, and LLM-specific instrumentation patterns, see &lt;a href="https://www.glukhov.org/observability/observability-for-llm-systems/" rel="noopener noreferrer"&gt;Observability for LLM Systems&lt;/a&gt; and the broader &lt;a href="https://www.glukhov.org/observability/" rel="noopener noreferrer"&gt;Observability&lt;/a&gt; pillar for how those signals fit into a production telemetry stack.&lt;br&gt;
When a user asks "why did the agent do that?", your answer should be a trace spanning orchestrator, A2A hops, MCP tool calls, and any &lt;code&gt;input_required&lt;/code&gt; pauses rather than a shrug and a log grep.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production Checklist for A2A Streaming and Async Tasks
&lt;/h2&gt;

&lt;p&gt;Before shipping long-running A2A paths to production, verify the following areas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent Card and capabilities&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;capabilities.streaming&lt;/code&gt; reflects actual SSE support&lt;/li&gt;
&lt;li&gt;[ ] Push notification support documented if implemented&lt;/li&gt;
&lt;li&gt;[ ] Skills that require human approval document expected &lt;code&gt;input_required&lt;/code&gt; behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Client modes&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] SSE client handles resubscription via SubscribeToTask&lt;/li&gt;
&lt;li&gt;[ ] Poll interval backs off under load&lt;/li&gt;
&lt;li&gt;[ ] Push webhook verifies authenticity and rejects stale events&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Durability&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Task state survives agent process restarts&lt;/li&gt;
&lt;li&gt;[ ] Artifacts stored outside ephemeral container filesystem&lt;/li&gt;
&lt;li&gt;[ ] Intermediate artifacts available for partial recovery&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Failure and policy&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Per-hop and end-to-end timeout budgets defined&lt;/li&gt;
&lt;li&gt;[ ] Retries idempotent for mutating operations&lt;/li&gt;
&lt;li&gt;[ ] Cancel propagates across delegation edges&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Observability&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] trace ID + task ID + agent ID on every hop&lt;/li&gt;
&lt;li&gt;[ ] State transitions logged&lt;/li&gt;
&lt;li&gt;[ ] Cost attribution per task or per agent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Load testing&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] SSE through your reverse proxy (buffering breaks streams)&lt;/li&gt;
&lt;li&gt;[ ] Concurrent long tasks without memory leaks on open connections&lt;/li&gt;
&lt;li&gt;[ ] Push flood handling without webhook overload&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;A2A's value shows up most clearly when work &lt;strong&gt;does not&lt;/strong&gt; fit a single synchronous API call, because streaming, async tasks, push notifications, and explicit task states are how the protocol handles real agent workloads such as research, delegation, approvals, and large artifacts without pretending everything completes in one HTTP round trip. Start with the simplest mode that works, add SSE when users need live progress, add push when connections cannot stay open, treat &lt;code&gt;input_required&lt;/code&gt; as a first-class design tool rather than a failure, and instrument every hop so multi-agent async workflows do not outrun your ability to explain them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;When should you use A2A streaming instead of polling?&lt;/strong&gt;&lt;br&gt;
Use streaming when the client can hold an open HTTP connection and you need low-latency progress updates or incremental artifacts. Use polling when connections are unreliable, clients are batch-oriented, or you only need periodic status checks on long-running tasks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What does input_required mean in an A2A task?&lt;/strong&gt;&lt;br&gt;
It is a pause state where the agent needs more information or human approval. Design UX and timeouts around it explicitly rather than treating it as an error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do A2A push notifications work?&lt;/strong&gt;&lt;br&gt;
Register a PushNotificationConfig with an HTTPS webhook. The server POSTs on significant updates; the client calls GetTask to retrieve full state and artifacts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How should you retry failed A2A tasks?&lt;/strong&gt;&lt;br&gt;
Retry transient failures with idempotency keys, respect timeout budgets, and do not blindly retry terminal states like rejected or policy failures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What should you log for long-running A2A workflows?&lt;/strong&gt;&lt;br&gt;
Correlate trace ID, task ID, and agent ID across hops. Log state transitions, artifacts, delegation, approvals, and per-step cost so you can reconstruct the full workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A2A Protocol -- Streaming and Asynchronous Operations: &lt;a href="https://a2a-protocol.org/latest/topics/streaming-and-async/" rel="noopener noreferrer"&gt;https://a2a-protocol.org/latest/topics/streaming-and-async/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;A2A Protocol -- Specification overview: &lt;a href="https://a2a-protocol.org/latest/specification/" rel="noopener noreferrer"&gt;https://a2a-protocol.org/latest/specification/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>llm</category>
      <category>ai</category>
      <category>aicoding</category>
    </item>
    <item>
      <title>Run Docker Compose as a Linux Service with systemd</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Thu, 09 Jul 2026 11:37:13 +0000</pubDate>
      <link>https://dev.to/rosgluk/run-docker-compose-as-a-linux-service-with-systemd-2g8e</link>
      <guid>https://dev.to/rosgluk/run-docker-compose-as-a-linux-service-with-systemd-2g8e</guid>
      <description>&lt;p&gt;Docker Compose on a Linux server should start on boot, stop cleanly on shutdown, and survive reboots without manual intervention.&lt;/p&gt;

&lt;p&gt;Docker Compose is not Kubernetes, and that is fine for the workloads this guide targets. For many real systems, a Compose project on a single Linux host is the right amount of infrastructure — simple, readable, easy to back up, and good enough for internal tools, side projects, self-hosted services, staging environments, small production apps, and developer infrastructure.&lt;/p&gt;

&lt;p&gt;The missing piece is usually service management. Running this manually is not enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A single command starts the stack, but it does not document how the stack should start on boot, stop during shutdown, reload after changes, write logs, recover from failures, or get updated safely. That is where systemd helps.&lt;/p&gt;

&lt;p&gt;This guide walks through running a Docker Compose project as a Linux service with systemd — unit files, boot ordering, updates, logs, and backups. The split of responsibility is deliberate: Docker runs containers, Compose defines the stack, and systemd starts and stops the project on the host. It is part of &lt;a href="https://www.glukhov.org/developer-tools/" rel="noopener noreferrer"&gt;Developer Tools - a Guide to Development Workflows&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Docker Compose as a Service Makes Sense
&lt;/h2&gt;

&lt;p&gt;Running Compose under systemd makes sense when you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A single Linux server&lt;/li&gt;
&lt;li&gt;A small self-hosted application&lt;/li&gt;
&lt;li&gt;A reverse proxy stack&lt;/li&gt;
&lt;li&gt;A monitoring stack&lt;/li&gt;
&lt;li&gt;A local development platform&lt;/li&gt;
&lt;li&gt;An internal tool&lt;/li&gt;
&lt;li&gt;A staging environment&lt;/li&gt;
&lt;li&gt;A simple production service with known limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Nginx Proxy Manager&lt;/li&gt;
&lt;li&gt;Traefik&lt;/li&gt;
&lt;li&gt;Gitea&lt;/li&gt;
&lt;li&gt;Grafana and Prometheus&lt;/li&gt;
&lt;li&gt;PostgreSQL plus a small web app&lt;/li&gt;
&lt;li&gt;Uptime Kuma&lt;/li&gt;
&lt;li&gt;Home Assistant helper services&lt;/li&gt;
&lt;li&gt;Private registry&lt;/li&gt;
&lt;li&gt;Internal API plus worker plus Redis&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Compose is a good fit when the operational model is still understandable by one person reading one directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Docker Compose Is Not Enough
&lt;/h2&gt;

&lt;p&gt;Use something else when you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multi-node scheduling&lt;/li&gt;
&lt;li&gt;Automatic rescheduling across hosts&lt;/li&gt;
&lt;li&gt;Cluster-level service discovery&lt;/li&gt;
&lt;li&gt;Horizontal autoscaling&lt;/li&gt;
&lt;li&gt;Rolling deployments across many machines&lt;/li&gt;
&lt;li&gt;Fine-grained workload identity&lt;/li&gt;
&lt;li&gt;Complex network policy&lt;/li&gt;
&lt;li&gt;Large multi-team platform operations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At that point, Kubernetes, Nomad, Swarm, or a managed platform may be a better fit.&lt;/p&gt;

&lt;p&gt;My practical rule is to avoid using Kubernetes just to skip learning systemd, and to avoid using Compose when the workload clearly needs orchestration across multiple hosts.&lt;/p&gt;

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

&lt;p&gt;A clean setup separates project files, the systemd unit, and persistent data on the host. The Compose project lives under &lt;code&gt;/opt/myapp/&lt;/code&gt; with &lt;code&gt;compose.yaml&lt;/code&gt;, &lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;data/&lt;/code&gt;, &lt;code&gt;backups/&lt;/code&gt;, and optional scripts such as &lt;code&gt;scripts/update.sh&lt;/code&gt;. The systemd unit file sits at &lt;code&gt;/etc/systemd/system/myapp.service&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TB
  subgraph host["Linux host"]
    systemd["systemd unit\n/etc/systemd/system/myapp.service"]
    compose["Docker Compose\n/opt/myapp/compose.yaml"]
    docker["Docker Engine"]
    fs["Persistent data\n/opt/myapp/data/"]
  end
  systemd --&amp;gt;|"ExecStart: docker compose up -d"| compose
  compose --&amp;gt; docker
  docker --&amp;gt; fs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each layer has a clear job: Docker runs containers, Compose defines the application stack, systemd starts and stops the Compose project on boot and shutdown, the host filesystem stores persistent data, backups stay explicit, and updates go through scripted, reviewable steps. This layout is deliberately boring, because boring infrastructure is easier to repair when something breaks at 2 a.m.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prepare the Compose Project Directory
&lt;/h2&gt;

&lt;p&gt;Create a directory under &lt;code&gt;/opt&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /opt/myapp
&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;:&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; /opt/myapp
&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/myapp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a Compose file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nano compose.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx:stable&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:80"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./html:/usr/share/nginx/html:ro&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nginx&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-t&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;||&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;exit&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
      &lt;span class="na"&gt;start_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the content directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; html
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Hello from Docker Compose"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; html/index.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test manually first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
docker compose ps
docker compose logs &lt;span class="nt"&gt;--tail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;50
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then stop it before handing lifecycle to systemd:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not create a systemd service until the Compose project works manually. While you test, keep the &lt;a href="https://www.glukhov.org/developer-tools/containers/docker-compose-cheatsheet/" rel="noopener noreferrer"&gt;Docker Compose Cheatsheet&lt;/a&gt; nearby for &lt;code&gt;ps&lt;/code&gt;, &lt;code&gt;logs&lt;/code&gt;, &lt;code&gt;pull&lt;/code&gt;, and project structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use the Modern &lt;code&gt;docker compose&lt;/code&gt; Command
&lt;/h2&gt;

&lt;p&gt;Docker Engine and the Compose plugin must be installed before you write a unit file. On Ubuntu, &lt;a href="https://www.glukhov.org/developer-tools/containers/install-docker-on-ubuntu/" rel="noopener noreferrer"&gt;Install Docker on Ubuntu&lt;/a&gt; walks through APT, Snap, rootless mode, and post-install security so you end up with a working &lt;code&gt;docker compose&lt;/code&gt; command.&lt;/p&gt;

&lt;p&gt;Use this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The old &lt;code&gt;docker-compose&lt;/code&gt; binary still exists on many machines, but modern Docker uses Compose as a Docker CLI plugin.&lt;/p&gt;

&lt;p&gt;In service files and scripts, prefer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/usr/bin/docker compose
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can find the Docker path with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Usually it is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/usr/bin/docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Create a systemd Service for Docker Compose
&lt;/h2&gt;

&lt;p&gt;If unit files are new to you, &lt;a href="https://www.glukhov.org/developer-tools/terminals-shell/executable-as-a-service-in-linux/" rel="noopener noreferrer"&gt;Run any Executable as a Service in Linux&lt;/a&gt; explains &lt;code&gt;Type&lt;/code&gt;, &lt;code&gt;ExecStart&lt;/code&gt;, &lt;code&gt;systemctl&lt;/code&gt;, and the general systemd workflow. This section applies those patterns specifically to a Compose stack.&lt;/p&gt;

&lt;p&gt;Create the service file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/systemd/system/myapp.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use this unit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;MyApp Docker Compose stack&lt;/span&gt;
&lt;span class="py"&gt;Requires&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.service&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.service network-online.target&lt;/span&gt;
&lt;span class="py"&gt;Wants&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;span class="py"&gt;RemainAfterExit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/myapp&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose up -d --remove-orphans&lt;/span&gt;
&lt;span class="py"&gt;ExecStop&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose down&lt;/span&gt;
&lt;span class="py"&gt;TimeoutStartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;
&lt;span class="py"&gt;TimeoutStopSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;120&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reload systemd:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start the service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start myapp.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enable it on boot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;myapp.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl status myapp.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check containers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/myapp
docker compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why Type=oneshot and RemainAfterExit=yes?
&lt;/h2&gt;

&lt;p&gt;This is the part many guides get subtly wrong.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;docker compose up -d&lt;/code&gt; starts containers in detached mode and exits, so there is no long-running foreground Compose process for systemd to supervise. The systemd unit should not pretend that &lt;code&gt;docker compose up -d&lt;/code&gt; is a long-running daemon.&lt;/p&gt;

&lt;p&gt;Use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;span class="py"&gt;RemainAfterExit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells systemd:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run the start command.&lt;/li&gt;
&lt;li&gt;Consider the unit active after the command exits successfully.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;ExecStop&lt;/code&gt; when the service is stopped.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That matches the actual behavior of detached Compose, which is why &lt;code&gt;Type=oneshot&lt;/code&gt; with &lt;code&gt;RemainAfterExit=yes&lt;/code&gt; is the right default for most stacks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Not Type=simple?
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;Type=simple&lt;/code&gt;, systemd expects the &lt;code&gt;ExecStart&lt;/code&gt; process to keep running, but &lt;code&gt;docker compose up -d&lt;/code&gt; exits after starting containers. That can make systemd think the service ended, then call stop logic or mark the unit inactive depending on configuration.&lt;/p&gt;

&lt;p&gt;If you want &lt;code&gt;Type=simple&lt;/code&gt;, you would usually run Compose in the foreground:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose up&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That can work, but I usually do not prefer it for Compose stacks on servers. Detached containers plus explicit &lt;code&gt;ExecStop&lt;/code&gt; are easier to operate.&lt;/p&gt;

&lt;h2&gt;
  
  
  A More Production-Friendly Unit
&lt;/h2&gt;

&lt;p&gt;For a real server, I prefer a slightly stricter unit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;MyApp Docker Compose stack&lt;/span&gt;
&lt;span class="py"&gt;Documentation&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;https://example.com/docs/myapp&lt;/span&gt;
&lt;span class="py"&gt;Requires&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.service&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.service network-online.target&lt;/span&gt;
&lt;span class="py"&gt;Wants&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;span class="py"&gt;RemainAfterExit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/myapp&lt;/span&gt;
&lt;span class="py"&gt;EnvironmentFile&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;-/opt/myapp/.env.systemd&lt;/span&gt;
&lt;span class="py"&gt;ExecStartPre&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose config --quiet&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose up -d --remove-orphans&lt;/span&gt;
&lt;span class="py"&gt;ExecReload&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose up -d --remove-orphans&lt;/span&gt;
&lt;span class="py"&gt;ExecStop&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose down&lt;/span&gt;
&lt;span class="py"&gt;TimeoutStartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;
&lt;span class="py"&gt;TimeoutStopSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;120&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Important details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;WorkingDirectory&lt;/code&gt; points to the Compose project.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ExecStartPre&lt;/code&gt; validates the Compose config.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ExecReload&lt;/code&gt; recreates changed services.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ExecStop&lt;/code&gt; stops and removes the Compose project containers and default network.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;EnvironmentFile=-...&lt;/code&gt; means the file is optional.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Create the optional systemd environment file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nano /opt/myapp/.env.systemd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;COMPOSE_PROJECT_NAME&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;myapp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then reload systemd:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart myapp.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Compose .env vs systemd EnvironmentFile
&lt;/h2&gt;

&lt;p&gt;Compose and systemd each have their own environment mechanism, and mixing them up causes confusing "variable not set" failures at boot.&lt;/p&gt;

&lt;p&gt;Compose automatically reads a &lt;code&gt;.env&lt;/code&gt; file in the project directory for variable substitution in the Compose file.&lt;/p&gt;

&lt;p&gt;Example &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;APP_TAG=1.2.3
WEB_PORT=8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example &lt;code&gt;compose.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx:${APP_TAG}&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${WEB_PORT}:80"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A systemd &lt;code&gt;EnvironmentFile&lt;/code&gt; sets environment variables for the &lt;code&gt;docker compose&lt;/code&gt; command itself.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;EnvironmentFile&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;-/opt/myapp/.env.systemd&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For many projects, you only need Compose &lt;code&gt;.env&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Use a systemd environment file when you want to define things such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;COMPOSE_PROJECT_NAME&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;myapp&lt;/span&gt;
&lt;span class="py"&gt;COMPOSE_FILE&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;compose.yaml&lt;/span&gt;
&lt;span class="py"&gt;DOCKER_HOST&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;unix:///var/run/docker.sock&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not use either file as a casual secrets vault. If secrets matter, use Docker secrets, an external secret manager, encrypted files, or at least strict permissions.&lt;/p&gt;

&lt;p&gt;Set restrictive permissions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod &lt;/span&gt;600 /opt/myapp/.env
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 /opt/myapp/.env.systemd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Restart Policies: Docker vs systemd
&lt;/h2&gt;

&lt;p&gt;There are two restart layers — container restart policy in Compose and systemd service restart policy — and they should not be mixed blindly.&lt;/p&gt;

&lt;p&gt;For long-running containers, set restart policies in Compose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx:stable&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Common restart values:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Policy&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;Do not restart automatically&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;always&lt;/td&gt;
&lt;td&gt;Restart after exit and daemon restart&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;on-failure&lt;/td&gt;
&lt;td&gt;Restart only after failure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;unless-stopped&lt;/td&gt;
&lt;td&gt;Restart unless manually stopped&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For most persistent services, I prefer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is predictable and respects intentional manual stops.&lt;/p&gt;

&lt;p&gt;The systemd unit itself should usually not restart repeatedly, because &lt;code&gt;docker compose up -d&lt;/code&gt; is not the running workload. The containers are.&lt;/p&gt;

&lt;p&gt;So avoid this unless you have a specific reason:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In most Compose-as-service units, let Docker handle container restarts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Health Checks
&lt;/h2&gt;

&lt;p&gt;Restart policies restart containers when processes exit. They do not magically fix every unhealthy application.&lt;/p&gt;

&lt;p&gt;Add health checks where they are useful:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example/app:latest&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curl&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-fsS&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;http://localhost:8080/health&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;||&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;exit&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
      &lt;span class="na"&gt;start_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;20s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check health:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inspect a container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker inspect container-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Health checks are especially useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Web apps&lt;/li&gt;
&lt;li&gt;Reverse proxies&lt;/li&gt;
&lt;li&gt;Databases&lt;/li&gt;
&lt;li&gt;Queues&lt;/li&gt;
&lt;li&gt;Internal APIs&lt;/li&gt;
&lt;li&gt;Workers with a health endpoint&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They are less useful when they only check that a process exists, because a process that is alive but wedged still looks healthy. A bad health check is just another lie in YAML.&lt;/p&gt;

&lt;h2&gt;
  
  
  Startup Order and depends_on
&lt;/h2&gt;

&lt;p&gt;Compose can define dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example/app:latest&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;

  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;postgres"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This can help startup ordering, but do not over-trust it. Applications should still handle retries — databases restart, networks flap, DNS takes time, and a resilient app retries connections instead of assuming perfect startup order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Logs: journalctl and docker compose logs
&lt;/h2&gt;

&lt;p&gt;Two log views cover most debugging: systemd captures the lifecycle of the unit itself, while Compose captures application output from running containers.&lt;/p&gt;

&lt;p&gt;systemd service logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; myapp.service &lt;span class="nt"&gt;-n&lt;/span&gt; 100 &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Follow systemd logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; myapp.service &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compose service logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/myapp
docker compose logs &lt;span class="nt"&gt;--tail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;100
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt;
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; web
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For most app debugging, &lt;code&gt;docker compose logs&lt;/code&gt; is more useful; for lifecycle debugging — start failures, unit crashes, permission errors — &lt;code&gt;journalctl&lt;/code&gt; is more useful. If &lt;code&gt;systemctl start myapp&lt;/code&gt; fails, check &lt;code&gt;journalctl&lt;/code&gt; first. If the stack starts but the app is broken, check &lt;code&gt;docker compose logs&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Log Rotation
&lt;/h2&gt;

&lt;p&gt;Docker logs can grow forever if you do not configure them.&lt;/p&gt;

&lt;p&gt;For small servers, configure Docker log rotation in &lt;code&gt;/etc/docker/daemon.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"log-driver"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"json-file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"log-opts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"max-size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10m"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"max-file"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart Docker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then restart the Compose stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart myapp.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This applies to newly created containers. Recreate containers if needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/myapp
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--force-recreate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Log rotation is not glamorous, but it is one of the easiest ways to prevent a disk-full outage on a small server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Updating a Compose Service
&lt;/h2&gt;

&lt;p&gt;A simple manual update flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/myapp
docker compose pull
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--remove-orphans&lt;/span&gt;
docker image prune &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If managed by systemd, you can use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl reload myapp.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your unit has:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;ExecReload&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose up -d --remove-orphans&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But note: &lt;code&gt;ExecReload&lt;/code&gt; does not pull images unless you include that step.&lt;/p&gt;

&lt;p&gt;For explicit updates, create a script.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /opt/myapp/scripts
nano /opt/myapp/scripts/update.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/myapp

docker compose config &lt;span class="nt"&gt;--quiet&lt;/span&gt;
docker compose pull
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--remove-orphans&lt;/span&gt;
docker image prune &lt;span class="nt"&gt;-f&lt;/span&gt;
docker compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make it executable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x /opt/myapp/scripts/update.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/opt/myapp/scripts/update.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the service unit can remain focused on lifecycle, while the update script handles deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Safer Update Script with Backup Hook
&lt;/h2&gt;

&lt;p&gt;For stateful services, update only after backup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;APP_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/opt/myapp"&lt;/span&gt;
&lt;span class="nv"&gt;BACKUP_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/opt/myapp/backups"&lt;/span&gt;

&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$APP_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BACKUP_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Validating compose file"&lt;/span&gt;
docker compose config &lt;span class="nt"&gt;--quiet&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Running backup hook"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-x&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$APP_DIR&lt;/span&gt;&lt;span class="s2"&gt;/scripts/backup.sh"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$APP_DIR&lt;/span&gt;&lt;span class="s2"&gt;/scripts/backup.sh"&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"No backup hook found"&lt;/span&gt;
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Pulling images"&lt;/span&gt;
docker compose pull

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Recreating services"&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--remove-orphans&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Pruning unused images"&lt;/span&gt;
docker image prune &lt;span class="nt"&gt;-f&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Current status"&lt;/span&gt;
docker compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is still simple, but now it encodes an operational habit: backup before change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stopping the Service
&lt;/h2&gt;

&lt;p&gt;Stop the stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl stop myapp.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, &lt;code&gt;docker compose down&lt;/code&gt; removes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Containers for services in the Compose file&lt;/li&gt;
&lt;li&gt;Networks defined by the Compose file&lt;/li&gt;
&lt;li&gt;The default network&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It does not remove named volumes unless you ask it to.&lt;/p&gt;

&lt;p&gt;Do not casually use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose down &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That removes named volumes declared in the Compose file and anonymous volumes attached to containers. For databases and stateful apps, that can mean deleting real data.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;down -v&lt;/code&gt; only when you mean "destroy this environment".&lt;/p&gt;

&lt;h2&gt;
  
  
  Restarting the Service
&lt;/h2&gt;

&lt;p&gt;Restart the systemd unit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart myapp.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This runs the stop command and then the start command.&lt;/p&gt;

&lt;p&gt;For only restarting containers without recreating them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/myapp
docker compose restart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Important distinction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;docker compose restart&lt;/code&gt; restarts existing containers.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docker compose up -d&lt;/code&gt; applies config or image changes by recreating containers when needed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you changed &lt;code&gt;compose.yaml&lt;/code&gt;, use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not just:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose restart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Handling Orphan Containers
&lt;/h2&gt;

&lt;p&gt;If you rename or remove a service in &lt;code&gt;compose.yaml&lt;/code&gt;, old containers may remain as orphans.&lt;/p&gt;

&lt;p&gt;Use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--remove-orphans&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is why the systemd service examples in this guide use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose up -d --remove-orphans&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It keeps the stack closer to the current Compose file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backups
&lt;/h2&gt;

&lt;p&gt;Backups depend on the workload, but the principles are stable.&lt;/p&gt;

&lt;p&gt;For bind mounts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/opt/myapp/data/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Back up that directory.&lt;/p&gt;

&lt;p&gt;For named volumes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker volume &lt;span class="nb"&gt;ls&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inspect a volume:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker volume inspect volume-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For databases, filesystem copies are not always enough. Use application-aware backups:&lt;/p&gt;

&lt;p&gt;PostgreSQL example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-T&lt;/span&gt; db pg_dump &lt;span class="nt"&gt;-U&lt;/span&gt; postgres appdb &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; backups/appdb.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MariaDB example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-T&lt;/span&gt; db mariadb-dump &lt;span class="nt"&gt;-u&lt;/span&gt; root &lt;span class="nt"&gt;-p&lt;/span&gt; appdb &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; backups/appdb.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Redis example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;redis redis-cli BGSAVE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A Compose stack without a backup plan is not a service — it is a temporary experiment that happens to have uptime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security Baseline
&lt;/h2&gt;

&lt;p&gt;For a small Compose service on Linux, start with this baseline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep the Compose project under &lt;code&gt;/opt/appname&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Use explicit image tags, not only &lt;code&gt;latest&lt;/code&gt;, when stability matters.&lt;/li&gt;
&lt;li&gt;Use bind mounts or named volumes deliberately.&lt;/li&gt;
&lt;li&gt;Do not expose ports you do not need.&lt;/li&gt;
&lt;li&gt;Put public services behind a reverse proxy.&lt;/li&gt;
&lt;li&gt;Use HTTPS at the edge.&lt;/li&gt;
&lt;li&gt;Keep secrets out of Git.&lt;/li&gt;
&lt;li&gt;Restrict &lt;code&gt;.env&lt;/code&gt; permissions.&lt;/li&gt;
&lt;li&gt;Avoid privileged containers unless truly required.&lt;/li&gt;
&lt;li&gt;Avoid mounting the Docker socket into containers.&lt;/li&gt;
&lt;li&gt;Keep Docker and images updated.&lt;/li&gt;
&lt;li&gt;Test firewall behavior from another machine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A dangerous pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/run/docker.sock:/var/run/docker.sock&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives the container control over Docker. In practice, that can become host-level control. Use it only when you understand the risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resource Limits
&lt;/h2&gt;

&lt;p&gt;On small servers, one bad container can consume the host.&lt;/p&gt;

&lt;p&gt;Compose supports resource-related settings, but behavior can depend on Docker Engine and Compose version. For simple protection, start with application-level limits and Docker logging limits.&lt;/p&gt;

&lt;p&gt;For some workloads, you can add memory limits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example/app:stable&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;mem_limit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;512m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also configure app-level worker counts, queue limits, and cache sizes. Container limits are useful, but they are not a substitute for understanding the application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: A Realistic Compose Service
&lt;/h2&gt;

&lt;p&gt;Directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/opt/whoami/
  compose.yaml
  .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compose file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;whoami&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;traefik/whoami:v1.10&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${WHOAMI_PORT}:80"&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wget&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-qO-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;http://localhost&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;||&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;exit&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;.env&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WHOAMI_PORT=8080
COMPOSE_PROJECT_NAME=whoami
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;systemd unit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Whoami Docker Compose stack&lt;/span&gt;
&lt;span class="py"&gt;Requires&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.service&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.service network-online.target&lt;/span&gt;
&lt;span class="py"&gt;Wants&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;span class="py"&gt;RemainAfterExit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/whoami&lt;/span&gt;
&lt;span class="py"&gt;ExecStartPre&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose config --quiet&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose up -d --remove-orphans&lt;/span&gt;
&lt;span class="py"&gt;ExecReload&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose up -d --remove-orphans&lt;/span&gt;
&lt;span class="py"&gt;ExecStop&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose down&lt;/span&gt;
&lt;span class="py"&gt;TimeoutStartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;
&lt;span class="py"&gt;TimeoutStopSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;120&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; whoami.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl status whoami.service
&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/whoami
docker compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Service Starts but Containers Are Not Running
&lt;/h3&gt;

&lt;p&gt;Check systemd:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; myapp.service &lt;span class="nt"&gt;-n&lt;/span&gt; 100 &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Validate Compose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/myapp
docker compose config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check Docker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl status docker
docker info
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  WorkingDirectory Is Wrong
&lt;/h3&gt;

&lt;p&gt;If systemd cannot find your Compose file, confirm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/myapp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /opt/myapp
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /opt/myapp/compose.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The service runs from &lt;code&gt;WorkingDirectory&lt;/code&gt;, not from your current shell directory.&lt;/p&gt;

&lt;h3&gt;
  
  
  Docker Permission Denied
&lt;/h3&gt;

&lt;p&gt;If the unit runs as root, it can normally access Docker.&lt;/p&gt;

&lt;p&gt;If you set &lt;code&gt;User=someuser&lt;/code&gt;, that user must be able to access Docker. Usually that means membership in the &lt;code&gt;docker&lt;/code&gt; group, or a rootless Docker setup.&lt;/p&gt;

&lt;p&gt;Check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;groups &lt;/span&gt;someuser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the user if appropriate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; docker someuser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Be careful. The Docker group is effectively privileged.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compose Command Not Found
&lt;/h3&gt;

&lt;p&gt;Find Docker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use the full path in the unit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose up -d --remove-orphans&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If Compose plugin is missing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install it using your Docker package source.&lt;/p&gt;

&lt;h3&gt;
  
  
  Environment Variables Are Missing
&lt;/h3&gt;

&lt;p&gt;Check the Compose config as systemd would see it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/myapp
docker compose config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If systemd needs extra environment variables, use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;EnvironmentFile&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;-/opt/myapp/.env.systemd&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If Compose needs variables for substitution, use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/opt/myapp/.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are related, but not identical.&lt;/p&gt;

&lt;h3&gt;
  
  
  Containers Do Not Start After Reboot
&lt;/h3&gt;

&lt;p&gt;Check whether the systemd service is enabled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl is-enabled myapp.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enable it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;myapp.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check Docker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl is-enabled docker
systemctl status docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check boot logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; myapp.service &lt;span class="nt"&gt;-b&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  App Starts Before Database Is Ready
&lt;/h3&gt;

&lt;p&gt;Add a database health check and &lt;code&gt;depends_on&lt;/code&gt; with &lt;code&gt;service_healthy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Also fix the application. It should retry database connections. Infrastructure startup ordering is helpful, but application retry logic is better.&lt;/p&gt;

&lt;h3&gt;
  
  
  Disk Filled with Docker Logs
&lt;/h3&gt;

&lt;p&gt;Check Docker disk usage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker system &lt;span class="nb"&gt;df&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check large container logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo du&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; /var/lib/docker/containers | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configure Docker log rotation in &lt;code&gt;/etc/docker/daemon.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then recreate containers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Mistake 1: Running docker compose up in rc.local
&lt;/h3&gt;

&lt;p&gt;Running &lt;code&gt;docker compose up&lt;/code&gt; from &lt;code&gt;rc.local&lt;/code&gt; or a login script works until it does not — use a proper systemd unit instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 2: Using Restart=always in systemd and restart: always in Compose
&lt;/h3&gt;

&lt;p&gt;Usually you only need container restart policies in Compose. Avoid two supervisors fighting each other.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 3: Forgetting --remove-orphans
&lt;/h3&gt;

&lt;p&gt;Service renames and removals can leave old containers behind. Use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--remove-orphans&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Mistake 4: Using docker compose restart After Config Changes
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;restart&lt;/code&gt; restarts containers. It does not apply all configuration changes.&lt;/p&gt;

&lt;p&gt;Use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Mistake 5: Running down -v Without Thinking
&lt;/h3&gt;

&lt;p&gt;This can delete volumes. For stateful services, that can mean deleting data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 6: No Backup Before Pull
&lt;/h3&gt;

&lt;p&gt;New images can break. Databases can migrate. Tags can move. Back up first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 7: Publishing Every Port
&lt;/h3&gt;

&lt;p&gt;Only publish what the host needs to expose. Internal service-to-service traffic can stay on the Compose network.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Recommended Pattern
&lt;/h2&gt;

&lt;p&gt;For most single-host Linux services, use this pattern:&lt;/p&gt;

&lt;p&gt;Compose file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example/app:stable&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:8080"&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;systemd unit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;MyApp Docker Compose stack&lt;/span&gt;
&lt;span class="py"&gt;Requires&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.service&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;docker.service network-online.target&lt;/span&gt;
&lt;span class="py"&gt;Wants&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;span class="py"&gt;RemainAfterExit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/myapp&lt;/span&gt;
&lt;span class="py"&gt;ExecStartPre&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose config --quiet&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose up -d --remove-orphans&lt;/span&gt;
&lt;span class="py"&gt;ExecReload&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose up -d --remove-orphans&lt;/span&gt;
&lt;span class="py"&gt;ExecStop&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/docker compose down&lt;/span&gt;
&lt;span class="py"&gt;TimeoutStartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;
&lt;span class="py"&gt;TimeoutStopSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;120&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enable it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; myapp.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Operate it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status myapp.service
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart myapp.service
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; myapp.service &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/myapp &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern is not fancy, and that is the point. Docker Compose is excellent for small, understandable systems, systemd is excellent at starting and stopping host services, and together they give you a reliable single-server deployment model without pretending every project needs a cluster. For container-level commands outside Compose — images, volumes, networks, and cleanup — see the &lt;a href="https://www.glukhov.org/developer-tools/containers/docker-cheatsheet/" rel="noopener noreferrer"&gt;Docker Cheatsheet&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>selfhosting</category>
      <category>devops</category>
    </item>
    <item>
      <title>Install Docker on Ubuntu: APT, Snap, Rootless — Complete Guide 2026</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Wed, 08 Jul 2026 09:50:22 +0000</pubDate>
      <link>https://dev.to/rosgluk/install-docker-on-ubuntu-apt-snap-rootless-complete-guide-2026-4g35</link>
      <guid>https://dev.to/rosgluk/install-docker-on-ubuntu-apt-snap-rootless-complete-guide-2026-4g35</guid>
      <description>&lt;p&gt;Installing Docker on Ubuntu should be simple, but in practice several Docker-shaped options compete for the same command name, each with different packaging, upgrade behavior, and security implications.&lt;/p&gt;

&lt;p&gt;This guide compares every major install path so you can pick the one that fits your machine.&lt;/p&gt;

&lt;p&gt;The options you will encounter include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;docker.io&lt;/code&gt; from Ubuntu repositories&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docker-ce&lt;/code&gt; from Docker's official APT repository&lt;/li&gt;
&lt;li&gt;Docker from Snap&lt;/li&gt;
&lt;li&gt;Docker Desktop&lt;/li&gt;
&lt;li&gt;manually downloaded &lt;code&gt;.deb&lt;/code&gt; packages&lt;/li&gt;
&lt;li&gt;the Docker convenience script&lt;/li&gt;
&lt;li&gt;rootless Docker&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Although they all provide container tooling, they are not interchangeable packages. The best choice depends on whether the machine is a developer workstation, a CI runner, a small server, a self-hosting box, or a production host. My default recommendation is calm but firm: for most technical users on normal Ubuntu machines, install Docker Engine from Docker's official APT repository. Use Ubuntu's &lt;code&gt;docker.io&lt;/code&gt; only when distribution integration matters more than upstream Docker packaging. Avoid the Snap package unless you specifically want Snap behavior and understand its limits. Rootless Docker is worth knowing about, but it is not automatically the best default for every machine.&lt;/p&gt;

&lt;p&gt;This guide explains the tradeoffs, covers post-install security, and gives you clean installation paths for each method. Once Docker Engine is running, the &lt;a href="https://www.glukhov.org/developer-tools/containers/docker-cheatsheet/" rel="noopener noreferrer"&gt;Docker Cheatsheet&lt;/a&gt; is your daily command reference, and the &lt;a href="https://www.glukhov.org/developer-tools/containers/docker-compose-cheatsheet/" rel="noopener noreferrer"&gt;Docker Compose Cheatsheet&lt;/a&gt; covers multi-container setups. Both sit alongside Git, VS Code, and CI/CD guides in &lt;a href="https://www.glukhov.org/developer-tools/" rel="noopener noreferrer"&gt;Developer Tools: The Complete Guide to Modern Development Workflows&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Recommendation
&lt;/h2&gt;

&lt;p&gt;The table below summarizes which install path fits common scenarios.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use case&lt;/th&gt;
&lt;th&gt;Recommended install&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Developer workstation&lt;/td&gt;
&lt;td&gt;Docker official APT repo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI runner&lt;/td&gt;
&lt;td&gt;Docker official APT repo, version pinned if needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Small self-hosted server&lt;/td&gt;
&lt;td&gt;Docker official APT repo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production server&lt;/td&gt;
&lt;td&gt;Docker official APT repo, controlled upgrades&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ubuntu-only conservative system&lt;/td&gt;
&lt;td&gt;Ubuntu &lt;code&gt;docker.io&lt;/code&gt; package&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quick desktop experiment&lt;/td&gt;
&lt;td&gt;Docker Desktop or official APT repo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snap-managed Ubuntu setup&lt;/td&gt;
&lt;td&gt;Docker Snap, with caution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strong non-root daemon requirement&lt;/td&gt;
&lt;td&gt;Rootless Docker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Air-gapped host&lt;/td&gt;
&lt;td&gt;Manual &lt;code&gt;.deb&lt;/code&gt; packages or internal mirror&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you do not have a special reason to choose otherwise, Docker's official APT repository is the default.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Gets Installed
&lt;/h2&gt;

&lt;p&gt;A normal Docker Engine setup includes several moving parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker daemon: &lt;code&gt;dockerd&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Docker CLI: &lt;code&gt;docker&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;container runtime: &lt;code&gt;containerd&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;low-level runtime: &lt;code&gt;runc&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Buildx plugin: &lt;code&gt;docker buildx&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Compose plugin: &lt;code&gt;docker compose&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Modern Docker Compose is usually installed as a Docker CLI plugin. That means the command is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The old &lt;code&gt;docker-compose&lt;/code&gt; command still exists in older guides and older systems, but new Ubuntu setups should generally use the Compose plugin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 1: Install Docker from Docker's Official APT Repository
&lt;/h2&gt;

&lt;p&gt;This is the best default for most developers and DevOps users. You get Docker's upstream packaging, current Docker Engine releases, Buildx, Compose plugin, and a normal APT upgrade path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Remove Conflicting Packages First
&lt;/h3&gt;

&lt;p&gt;Before installing Docker CE, remove packages that may conflict with the official Docker packages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt remove docker.io docker-compose docker-compose-v2 docker-doc podman-docker containerd runc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is fine if APT says some of these packages are not installed.&lt;/p&gt;

&lt;p&gt;This command does not remove Docker images, containers, volumes, or networks stored under &lt;code&gt;/var/lib/docker&lt;/code&gt;. If you want a clean reset, that is a separate step and should be done deliberately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add Docker's Official APT Repository
&lt;/h3&gt;

&lt;p&gt;Install prerequisites:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;ca-certificates curl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the keyring directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 &lt;span class="nt"&gt;-d&lt;/span&gt; /etc/apt/keyrings
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Download Docker's repository key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://download.docker.com/linux/ubuntu/gpg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; /etc/apt/keyrings/docker.asc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Allow APT to read the key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;a+r /etc/apt/keyrings/docker.asc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the Docker repository using the deb822 &lt;code&gt;.sources&lt;/code&gt; format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/sources.list.d/docker.sources &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; /etc/os-release &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;UBUNTU_CODENAME&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;$VERSION_CODENAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;
Components: stable
Architectures: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;dpkg &lt;span class="nt"&gt;--print-architecture&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;
Signed-By: /etc/apt/keyrings/docker.asc
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update APT metadata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Install Docker Engine, Buildx, and Compose
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it is not running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;docker run hello-world
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check versions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nt"&gt;--version&lt;/span&gt;
docker buildx version
docker compose version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point Docker works, though you still need &lt;code&gt;sudo&lt;/code&gt; for most commands unless you configure non-root access in the post-install section below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 2: Install Docker from Ubuntu Repositories
&lt;/h2&gt;

&lt;p&gt;Ubuntu provides the &lt;code&gt;docker.io&lt;/code&gt; package, which you can install with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;docker.io docker-compose-v2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start and enable Docker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;docker run hello-world
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  When Ubuntu &lt;code&gt;docker.io&lt;/code&gt; Makes Sense
&lt;/h3&gt;

&lt;p&gt;Ubuntu's package can be a good choice when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You prefer Ubuntu-maintained packages.&lt;/li&gt;
&lt;li&gt;You want a version aligned with Ubuntu's release process.&lt;/li&gt;
&lt;li&gt;You are managing many Ubuntu hosts with standard repositories.&lt;/li&gt;
&lt;li&gt;You do not need the newest upstream Docker release.&lt;/li&gt;
&lt;li&gt;You want fewer third-party APT sources.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a reasonable choice. It is not "wrong."&lt;/p&gt;

&lt;h3&gt;
  
  
  When Ubuntu &lt;code&gt;docker.io&lt;/code&gt; Is Not Ideal
&lt;/h3&gt;

&lt;p&gt;Use Docker's official repository instead when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want the current upstream Docker Engine.&lt;/li&gt;
&lt;li&gt;You follow Docker's own documentation.&lt;/li&gt;
&lt;li&gt;You rely on current Buildx and Compose behavior.&lt;/li&gt;
&lt;li&gt;You want Docker CE package names.&lt;/li&gt;
&lt;li&gt;You are debugging issues against upstream Docker docs.&lt;/li&gt;
&lt;li&gt;You need predictable Docker-version parity across distributions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My bias: for developer machines and container-heavy hosts, use Docker's official APT repository. For conservative Ubuntu-managed machines, &lt;code&gt;docker.io&lt;/code&gt; is acceptable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 3: Install Docker with Snap
&lt;/h2&gt;

&lt;p&gt;The Docker snap installs with a single command, but simplicity does not always mean predictable behavior on a server or development machine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;snap &lt;span class="nb"&gt;install &lt;/span&gt;docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Snap packages have their own packaging model, update behavior, confinement assumptions, and filesystem layout. That is fine for many desktop apps, but Docker Engine is already a system-level container runtime, so the extra Snap layer can surprise people. If you manage other software with Snap, the &lt;a href="https://www.glukhov.org/developer-tools/package-management/snap-cheatsheet-package-manager-ubuntu/" rel="noopener noreferrer"&gt;Snap Package Manager Cheatsheet&lt;/a&gt; explains channels, confinement, and update behavior in more detail.&lt;/p&gt;

&lt;h3&gt;
  
  
  When Docker Snap Makes Sense
&lt;/h3&gt;

&lt;p&gt;Docker Snap may be reasonable when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You intentionally manage software with Snap.&lt;/li&gt;
&lt;li&gt;You are using Ubuntu Core or a Snap-heavy environment.&lt;/li&gt;
&lt;li&gt;You want snap-style automatic updates.&lt;/li&gt;
&lt;li&gt;You are experimenting and do not care about matching Docker's upstream APT instructions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why I Usually Avoid Docker Snap
&lt;/h3&gt;

&lt;p&gt;I usually avoid the Docker snap for development and server use because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Most Docker documentation assumes the standard Docker Engine layout.&lt;/li&gt;
&lt;li&gt;Troubleshooting paths can differ from APT installs.&lt;/li&gt;
&lt;li&gt;Service management may feel less transparent.&lt;/li&gt;
&lt;li&gt;Snap auto-updates can be inconvenient for infrastructure software.&lt;/li&gt;
&lt;li&gt;Some bind mounts, sockets, and host integration details may surprise you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If Docker is central to your workflow, install it like infrastructure rather than a casual desktop app — even when a Snap install looks tempting on the surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 4: Install Docker from Manual &lt;code&gt;.deb&lt;/code&gt; Packages
&lt;/h2&gt;

&lt;p&gt;Manual &lt;code&gt;.deb&lt;/code&gt; installation is useful when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The machine cannot use external APT repositories.&lt;/li&gt;
&lt;li&gt;You are building an offline install process.&lt;/li&gt;
&lt;li&gt;You mirror packages internally.&lt;/li&gt;
&lt;li&gt;You need strict change control.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tradeoff is maintenance, because you must download and install new packages manually whenever you upgrade.&lt;/p&gt;

&lt;p&gt;A manual install usually requires these packages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;containerd.io&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker-ce&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker-ce-cli&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker-buildx-plugin&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker-compose-plugin&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Install them with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dpkg &lt;span class="nt"&gt;-i&lt;/span&gt; ./containerd.io_&lt;span class="k"&gt;*&lt;/span&gt;.deb &lt;span class="se"&gt;\&lt;/span&gt;
  ./docker-ce_&lt;span class="k"&gt;*&lt;/span&gt;.deb &lt;span class="se"&gt;\&lt;/span&gt;
  ./docker-ce-cli_&lt;span class="k"&gt;*&lt;/span&gt;.deb &lt;span class="se"&gt;\&lt;/span&gt;
  ./docker-buildx-plugin_&lt;span class="k"&gt;*&lt;/span&gt;.deb &lt;span class="se"&gt;\&lt;/span&gt;
  ./docker-compose-plugin_&lt;span class="k"&gt;*&lt;/span&gt;.deb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then fix missing dependencies if needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nt"&gt;--fix-broken&lt;/span&gt; &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status docker
&lt;span class="nb"&gt;sudo &lt;/span&gt;docker run hello-world
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Manual &lt;code&gt;.deb&lt;/code&gt; installs are not my first choice, but they remain valid for controlled or air-gapped environments where explicit package approval matters more than convenience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 5: Use Docker's Convenience Script
&lt;/h2&gt;

&lt;p&gt;Docker provides a convenience script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://get.docker.com &lt;span class="nt"&gt;-o&lt;/span&gt; get-docker.sh
&lt;span class="nb"&gt;sudo &lt;/span&gt;sh get-docker.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can preview what it would do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;sh get-docker.sh &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The convenience script is useful for disposable test machines, demos, labs, and throwaway environments, but I would not use it as the main install method for production systems. A script that configures repositories and installs packages non-interactively is convenient, yet long-lived infrastructure deserves explicit, reviewable steps — so for production hosts, use the APT repository method directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Docker Desktop vs Docker Engine on Ubuntu
&lt;/h2&gt;

&lt;p&gt;Docker Desktop for Linux is a different product from Docker Engine. Docker Engine is the server-side runtime and CLI workflow most Linux server users expect, while Docker Desktop adds a GUI, Desktop integrations, and a product experience closer to macOS and Windows Docker usage.&lt;/p&gt;

&lt;p&gt;Use Docker Desktop when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want a graphical Docker experience.&lt;/li&gt;
&lt;li&gt;You want Desktop features.&lt;/li&gt;
&lt;li&gt;You are aligning with a team that standardizes on Docker Desktop.&lt;/li&gt;
&lt;li&gt;You do not mind the extra layer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use Docker Engine when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You are running a server.&lt;/li&gt;
&lt;li&gt;You want a simple Linux-native runtime.&lt;/li&gt;
&lt;li&gt;You prefer systemd-managed services.&lt;/li&gt;
&lt;li&gt;You are building CI, DevOps, or self-hosted infrastructure.&lt;/li&gt;
&lt;li&gt;You do not need the GUI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For an advanced technical blog audience, Docker Engine is usually the more interesting default on Linux servers and CI hosts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Post-Install: Run Docker Without Sudo
&lt;/h2&gt;

&lt;p&gt;After installation, this works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;docker ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But this may fail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is because the Docker daemon listens on a Unix socket owned by root. The common fix is to add your user to the &lt;code&gt;docker&lt;/code&gt; group.&lt;/p&gt;

&lt;p&gt;Create the group if needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;groupadd docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add your user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; docker &lt;span class="nv"&gt;$USER&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply the new group membership:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;newgrp docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or log out and log back in.&lt;/p&gt;

&lt;p&gt;Test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run hello-world
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Important Security Note About the Docker Group
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;docker&lt;/code&gt; group is not a harmless convenience group. A user who can control the Docker daemon can usually get root-equivalent control of the host, so for a personal developer machine this is often acceptable, but on a shared server it is a serious access-control decision. Treat membership in the &lt;code&gt;docker&lt;/code&gt; group like admin access, and if that feels too broad, consider rootless Docker instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rootless Docker on Ubuntu
&lt;/h2&gt;

&lt;p&gt;Rootless Docker runs the Docker daemon and containers as a non-root user. This is not the same as adding your user to the &lt;code&gt;docker&lt;/code&gt; group — with the &lt;code&gt;docker&lt;/code&gt; group, the daemon still runs as root, whereas in rootless mode the daemon itself runs as your user.&lt;/p&gt;

&lt;h3&gt;
  
  
  When Rootless Docker Makes Sense
&lt;/h3&gt;

&lt;p&gt;Rootless Docker is useful when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want to reduce daemon-level root risk.&lt;/li&gt;
&lt;li&gt;You are on a shared development machine.&lt;/li&gt;
&lt;li&gt;You run user-owned containers.&lt;/li&gt;
&lt;li&gt;You do not need every advanced networking and storage feature.&lt;/li&gt;
&lt;li&gt;You want a safer default for experimental workloads.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  When Rootless Docker May Be Annoying
&lt;/h3&gt;

&lt;p&gt;Rootless Docker can be less convenient when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need privileged containers.&lt;/li&gt;
&lt;li&gt;You rely on low port binding without extra setup.&lt;/li&gt;
&lt;li&gt;You need some host networking patterns.&lt;/li&gt;
&lt;li&gt;You expect behavior identical to rootful Docker.&lt;/li&gt;
&lt;li&gt;You are following guides written for normal Docker Engine installs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rootless mode improves security posture, but it is not zero friction compared with a standard rootful install.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install Rootless Docker
&lt;/h3&gt;

&lt;p&gt;Install prerequisites:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;uidmap
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you installed Docker from DEB or APT packages, the rootless setup tool should be available:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dockerd-rootless-setuptool.sh &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If rootful Docker is already running and you want only rootless Docker, disable the system daemon:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl disable &lt;span class="nt"&gt;--now&lt;/span&gt; docker.service docker.socket
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You may also need to remove the rootful socket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/run/docker.sock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After installing rootless Docker, the setup tool usually prints environment variables to add to your shell profile. They commonly look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/bin:&lt;span class="nv"&gt;$PATH&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;DOCKER_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;unix:///run/user/1000/docker.sock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your UID may differ. Check the exact output from the setup tool.&lt;/p&gt;

&lt;p&gt;Enable lingering if you want the user service to run after logout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;loginctl enable-linger &lt;span class="nv"&gt;$USER&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the user service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; status docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run a test container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run hello-world
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Rootful Docker vs Rootless Docker
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Rootful Docker&lt;/th&gt;
&lt;th&gt;Rootless Docker&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Daemon user&lt;/td&gt;
&lt;td&gt;root&lt;/td&gt;
&lt;td&gt;normal user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Command convenience&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compatibility&lt;/td&gt;
&lt;td&gt;highest&lt;/td&gt;
&lt;td&gt;good, but not perfect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security posture&lt;/td&gt;
&lt;td&gt;weaker by default&lt;/td&gt;
&lt;td&gt;better by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Privileged containers&lt;/td&gt;
&lt;td&gt;supported&lt;/td&gt;
&lt;td&gt;limited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Low ports&lt;/td&gt;
&lt;td&gt;simple&lt;/td&gt;
&lt;td&gt;needs extra setup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server usage&lt;/td&gt;
&lt;td&gt;common&lt;/td&gt;
&lt;td&gt;possible, but plan carefully&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Developer workstation&lt;/td&gt;
&lt;td&gt;common&lt;/td&gt;
&lt;td&gt;good for security-conscious users&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;My practical recommendation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use normal rootful Docker for a personal dev machine or simple server.&lt;/li&gt;
&lt;li&gt;Add only trusted users to the &lt;code&gt;docker&lt;/code&gt; group.&lt;/li&gt;
&lt;li&gt;Use rootless Docker when user isolation matters.&lt;/li&gt;
&lt;li&gt;Do not pretend the &lt;code&gt;docker&lt;/code&gt; group is a security boundary.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Enable Docker at Boot
&lt;/h2&gt;

&lt;p&gt;On Ubuntu, Docker installed from normal packages usually starts automatically, but it is worth confirming after a fresh install or migration.&lt;/p&gt;

&lt;p&gt;Check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl is-enabled docker
systemctl is-enabled containerd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enable manually if needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;docker.service
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;containerd.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Disable auto-start:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl disable docker.service
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl disable containerd.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For rootless Docker, use the user service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nb"&gt;enable &lt;/span&gt;docker
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; start docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And enable lingering if needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;loginctl enable-linger &lt;span class="nv"&gt;$USER&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Install or Verify Docker Compose
&lt;/h2&gt;

&lt;p&gt;With Docker's official APT repository, install Compose as a plugin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;docker-compose-plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you installed Ubuntu's package, you may use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;docker-compose-v2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prefer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Over the old style:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hyphenated &lt;code&gt;docker-compose&lt;/code&gt; command belongs to the older standalone Compose era. Many systems still have it, but new documentation should use &lt;code&gt;docker compose&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check Which Docker You Have Installed
&lt;/h2&gt;

&lt;p&gt;When you inherit a machine or debug a broken setup, these commands help identify which Docker packaging is actually in use.&lt;/p&gt;

&lt;p&gt;Check the Docker binary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;which docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check package ownership:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dpkg &lt;span class="nt"&gt;-S&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;which docker&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;List Docker-related packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dpkg &lt;span class="nt"&gt;-l&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'docker|containerd|runc'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check APT policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-cache policy docker-ce docker.io containerd.io docker-compose-plugin docker-compose-v2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check Snap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;snap list | &lt;span class="nb"&gt;grep &lt;/span&gt;docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check service status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl status docker
systemctl status containerd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check Docker server details:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker info
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;docker info&lt;/code&gt; fails without &lt;code&gt;sudo&lt;/code&gt;, check your group membership:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;groups&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Migrate from Ubuntu &lt;code&gt;docker.io&lt;/code&gt; to Docker CE
&lt;/h2&gt;

&lt;p&gt;Stop Docker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl stop docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remove Ubuntu packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt remove docker.io docker-compose docker-compose-v2 docker-doc containerd runc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add Docker's official APT repository using the steps above.&lt;/p&gt;

&lt;p&gt;Install Docker CE:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start Docker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check existing containers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker ps &lt;span class="nt"&gt;-a&lt;/span&gt;
docker images
docker volume &lt;span class="nb"&gt;ls&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Normally, removing packages does not delete &lt;code&gt;/var/lib/docker&lt;/code&gt;, so your images, containers, and volumes may still exist. Still, back up important data first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migrate from Docker Snap to Docker CE
&lt;/h2&gt;

&lt;p&gt;First inspect what exists:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;snap list | &lt;span class="nb"&gt;grep &lt;/span&gt;docker
docker info
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stop workloads and back up important volumes or bind-mounted data.&lt;/p&gt;

&lt;p&gt;Remove the snap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;snap remove docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then install Docker CE from the official APT repository.&lt;/p&gt;

&lt;p&gt;Be careful with data locations. Snap packages often use different paths and confinement rules. Do not assume that Docker Snap data will appear automatically under the standard &lt;code&gt;/var/lib/docker&lt;/code&gt; path after migration.&lt;/p&gt;

&lt;p&gt;For important services, export or back up data explicitly before switching package sources.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pin a Docker Version with APT
&lt;/h2&gt;

&lt;p&gt;For production-like systems, you may want controlled upgrades.&lt;/p&gt;

&lt;p&gt;List available versions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt list &lt;span class="nt"&gt;--all-versions&lt;/span&gt; docker-ce
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install a specific version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;VERSION_STRING&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"5:29.0.0-1~ubuntu.24.04~noble"&lt;/span&gt;

&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;docker-ce&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$VERSION_STRING&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  docker-ce-cli&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$VERSION_STRING&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  containerd.io &lt;span class="se"&gt;\&lt;/span&gt;
  docker-buildx-plugin &lt;span class="se"&gt;\&lt;/span&gt;
  docker-compose-plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hold Docker packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-mark hold docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remove the hold later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-mark unhold docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do this when you need predictable change windows. For a personal laptop, it may be unnecessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Firewall and Networking Notes
&lt;/h2&gt;

&lt;p&gt;Docker modifies packet filtering rules to make container networking work, which matters if you use UFW, firewalld, nftables, or custom firewall rules.&lt;/p&gt;

&lt;p&gt;Important points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Published Docker ports may bypass naive UFW expectations.&lt;/li&gt;
&lt;li&gt;Docker uses iptables integration.&lt;/li&gt;
&lt;li&gt;Custom rules should account for Docker-created chains.&lt;/li&gt;
&lt;li&gt;Server hardening should be tested with real container port mappings.&lt;/li&gt;
&lt;li&gt;Do not assume &lt;code&gt;ufw deny&lt;/code&gt; protects a port published by Docker.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Test exposed ports from another machine, not only from localhost.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:80 nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then from another host:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://server-ip:8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On servers, Docker networking is part of your security model, not an implementation detail you can ignore after install.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Errors and Fixes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Permission Denied on Docker Socket
&lt;/h3&gt;

&lt;p&gt;Error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;permission denied while trying to connect to the Docker daemon socket
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fix options:&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;sudo&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;docker ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or add your user to the &lt;code&gt;docker&lt;/code&gt; group:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; docker &lt;span class="nv"&gt;$USER&lt;/span&gt;
newgrp docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remember that the &lt;code&gt;docker&lt;/code&gt; group is root-equivalent in practice, so treat group membership as a privileged access decision.&lt;/p&gt;

&lt;h3&gt;
  
  
  Docker Daemon Is Not Running
&lt;/h3&gt;

&lt;p&gt;Check status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; docker &lt;span class="nt"&gt;--no-pager&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Conflicting Docker Packages
&lt;/h3&gt;

&lt;p&gt;If installing Docker CE fails because of conflicts, remove old packages. If APT itself is in a bad state after adding the Docker repository, work through &lt;a href="https://www.glukhov.org/developer-tools/package-management/ubuntu-apt-troubleshooting-broken-packages-gpg/" rel="noopener noreferrer"&gt;Ubuntu APT troubleshooting for broken packages and GPG errors&lt;/a&gt; before retrying.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt remove docker.io docker-compose docker-compose-v2 docker-doc podman-docker containerd runc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then retry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Compose Command Not Found
&lt;/h3&gt;

&lt;p&gt;Check modern Compose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install plugin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;docker-compose-plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you expected the old command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You may be following old documentation. Prefer updating the command to &lt;code&gt;docker compose&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Old Root-Owned Docker Config
&lt;/h3&gt;

&lt;p&gt;If you ran Docker with &lt;code&gt;sudo&lt;/code&gt; before setting up group access, your user config may be owned by root.&lt;/p&gt;

&lt;p&gt;Fix ownership:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;:&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.docker"&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt;
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;g+rwx &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.docker"&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or remove the config if you do not need it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.docker"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It will be recreated.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cannot Connect to Rootless Docker
&lt;/h3&gt;

&lt;p&gt;Check environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DOCKER_HOST&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check user service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; status docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set the socket path if needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;DOCKER_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;unix:///run/user/&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;/docker.sock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add it to your shell profile only after confirming it is correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  Uninstall Docker Engine
&lt;/h2&gt;

&lt;p&gt;Remove Docker CE packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt purge docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin docker-ce-rootless-extras
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remove Docker data only if you really want to delete images, containers, and volumes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/docker
&lt;span class="nb"&gt;sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/containerd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remove Docker repository files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /etc/apt/sources.list.d/docker.sources
&lt;span class="nb"&gt;sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /etc/apt/keyrings/docker.asc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Refresh APT:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Docker Snap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;snap remove docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Ubuntu &lt;code&gt;docker.io&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt purge docker.io docker-compose-v2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Recommended Install Path for Most Ubuntu Users
&lt;/h2&gt;

&lt;p&gt;For most technical Ubuntu users, this is the clean path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt remove docker.io docker-compose docker-compose-v2 docker-doc podman-docker containerd runc

&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;ca-certificates curl

&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 &lt;span class="nt"&gt;-d&lt;/span&gt; /etc/apt/keyrings

&lt;span class="nb"&gt;sudo &lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://download.docker.com/linux/ubuntu/gpg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; /etc/apt/keyrings/docker.asc

&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;a+r /etc/apt/keyrings/docker.asc

&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/sources.list.d/docker.sources &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; /etc/os-release &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;UBUNTU_CODENAME&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;$VERSION_CODENAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;
Components: stable
Architectures: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;dpkg &lt;span class="nt"&gt;--print-architecture&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;
Signed-By: /etc/apt/keyrings/docker.asc
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update

&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

&lt;span class="nb"&gt;sudo &lt;/span&gt;docker run hello-world
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then decide whether you want group access for convenience or rootless Docker for tighter isolation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; docker &lt;span class="nv"&gt;$USER&lt;/span&gt;
newgrp docker
docker run hello-world
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Final Opinionated Guidance
&lt;/h2&gt;

&lt;p&gt;Docker installation on Ubuntu is an operational choice, not just a package choice. The method you pick affects upgrades, security boundaries, and how closely your host matches upstream Docker documentation.&lt;/p&gt;

&lt;p&gt;My practical rules are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use Docker's official APT repository for most developer and DevOps machines.&lt;/li&gt;
&lt;li&gt;Use Ubuntu &lt;code&gt;docker.io&lt;/code&gt; when Ubuntu repository consistency matters more than upstream freshness.&lt;/li&gt;
&lt;li&gt;Avoid Docker Snap for serious container workflows unless you intentionally want Snap behavior.&lt;/li&gt;
&lt;li&gt;Avoid the convenience script for production hosts.&lt;/li&gt;
&lt;li&gt;Use modern &lt;code&gt;docker compose&lt;/code&gt;, not old &lt;code&gt;docker-compose&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Treat the &lt;code&gt;docker&lt;/code&gt; group as privileged access.&lt;/li&gt;
&lt;li&gt;Consider rootless Docker when user isolation matters.&lt;/li&gt;
&lt;li&gt;Pin versions on production-like systems.&lt;/li&gt;
&lt;li&gt;Test firewall behavior when publishing ports on servers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most boring Docker install is usually the best one: official APT repo, explicit keyring, Compose plugin, systemd service, understood permissions, and no mystery packaging layer in the middle. Once the engine is in place, multi-service workloads — including self-hosted LLM stacks — are a natural next step; for example, see &lt;a href="https://www.glukhov.org/llm-hosting/ollama/ollama-in-docker-compose/" rel="noopener noreferrer"&gt;Ollama in Docker Compose&lt;/a&gt; for running local models in containers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read More
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.docker.com/engine/install/ubuntu/" rel="noopener noreferrer"&gt;https://docs.docker.com/engine/install/ubuntu/&lt;/a&gt; "Install Docker Engine on Ubuntu | Docker Docs"&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://packages.ubuntu.com/noble/docker.io?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;https://packages.ubuntu.com/noble/docker.io?utm_source=chatgpt.com&lt;/a&gt; "Ubuntu - Details of package docker.io in noble"&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>selfhosting</category>
      <category>devops</category>
    </item>
    <item>
      <title>Ubuntu APT Troubleshooting: Fix Broken Packages, Holds, and GPG Errors</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Tue, 07 Jul 2026 09:00:05 +0000</pubDate>
      <link>https://dev.to/rosgluk/ubuntu-apt-troubleshooting-fix-broken-packages-holds-and-gpg-errors-2k5</link>
      <guid>https://dev.to/rosgluk/ubuntu-apt-troubleshooting-fix-broken-packages-holds-and-gpg-errors-2k5</guid>
      <description>&lt;p&gt;APT failures are common on long-lived Ubuntu machines, and they usually appear after a release upgrade, a third-party repository change, a removed PPA, a manually installed &lt;code&gt;.deb&lt;/code&gt;, or an interrupted package installation.&lt;/p&gt;

&lt;p&gt;The error message can look dramatic, but most APT problems are not mysterious — they are state problems where APT's view of repositories, versions, and installed packages no longer agrees.&lt;/p&gt;

&lt;p&gt;APT is trying to answer four questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Which repositories are enabled?&lt;/li&gt;
&lt;li&gt;Which package versions are available?&lt;/li&gt;
&lt;li&gt;Which packages are already installed?&lt;/li&gt;
&lt;li&gt;Which package changes are allowed?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When those answers conflict, you get held packages, broken dependencies, missing public keys, bad PPA errors, or packages kept back during upgrade. This guide gives you a practical troubleshooting sequence for Ubuntu APT, written for developers, DevOps engineers, and Linux users who want to fix the system without blindly pasting random commands from forum threads. Pair it with our &lt;a href="https://www.glukhov.org/developer-tools/package-management/listing-installing-upgrading-packages-in-ubuntu-with-apt/" rel="noopener noreferrer"&gt;Ubuntu APT and dpkg cheatsheet&lt;/a&gt; for everyday install and upgrade commands, and browse the wider &lt;a href="https://www.glukhov.org/developer-tools/" rel="noopener noreferrer"&gt;Developer Tools&lt;/a&gt; collection for related Linux workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Short Version
&lt;/h2&gt;

&lt;p&gt;If your system is only mildly broken, start here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;dpkg &lt;span class="nt"&gt;--configure&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nt"&gt;--fix-broken&lt;/span&gt; &lt;span class="nb"&gt;install
sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt upgrade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If packages are kept back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt list &lt;span class="nt"&gt;--upgradable&lt;/span&gt;
apt-mark showhold
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt full-upgrade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a PPA or third-party repository is failing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; /etc/apt/sources.list.d/
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the failing repository line, then disable or fix that repository. If you see &lt;code&gt;NO_PUBKEY&lt;/code&gt;, do not blindly import random keys from a keyserver — find the official repository instructions, install the repository key into &lt;code&gt;/etc/apt/keyrings&lt;/code&gt;, and bind it to that repository with &lt;code&gt;signed-by&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before You Fix Anything: Read the APT Error First
&lt;/h2&gt;

&lt;p&gt;Run this first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not skip it. &lt;code&gt;apt update&lt;/code&gt; refreshes package metadata. It does not upgrade packages. It tells you whether Ubuntu can read all configured repositories and verify their metadata.&lt;/p&gt;

&lt;p&gt;Then check your Ubuntu version and codename — a stale release name in &lt;code&gt;/etc/apt/sources.list.d/&lt;/code&gt; is a frequent cause of 404 and Release file errors. If you are unsure which release you are on, see &lt;a href="https://www.glukhov.org/developer-tools/terminals-shell/check-linux-ubuntu-version/" rel="noopener noreferrer"&gt;how to check your Ubuntu version&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lsb_release &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/os-release
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also check what is upgradeable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt list &lt;span class="nt"&gt;--upgradable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And check whether any package is held:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-mark showhold
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you the first split in the decision tree — identifying the class of failure first makes troubleshooting easier because each class has a different first fix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repository problem: &lt;code&gt;apt update&lt;/code&gt; fails.&lt;/li&gt;
&lt;li&gt;Dependency problem: &lt;code&gt;apt update&lt;/code&gt; works, but install or upgrade fails.&lt;/li&gt;
&lt;li&gt;Held package problem: APT refuses to change specific packages.&lt;/li&gt;
&lt;li&gt;Mixed source problem: A PPA, manual &lt;code&gt;.deb&lt;/code&gt;, or old repository provides incompatible versions.&lt;/li&gt;
&lt;li&gt;Interrupted install problem: &lt;code&gt;dpkg&lt;/code&gt; has unpacked packages that were never configured.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common Ubuntu APT Failure Types
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Packages Kept Back
&lt;/h3&gt;

&lt;p&gt;A kept-back package is not always an error; it means APT chose not to upgrade a package using the current command. This often happens when the upgrade requires installing new dependencies, removing old packages, or changing a package in a way that plain &lt;code&gt;apt upgrade&lt;/code&gt; will not perform.&lt;/p&gt;

&lt;p&gt;Check details:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt list &lt;span class="nt"&gt;--upgradable&lt;/span&gt;
apt-cache policy package-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Try a full upgrade only after reading what APT wants to do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt full-upgrade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;full-upgrade&lt;/code&gt; is allowed to install new packages and remove packages if needed to complete the upgrade. That is useful, but it is also why you should read the proposed changes before accepting. On a workstation, &lt;code&gt;full-upgrade&lt;/code&gt; is usually fine after review; on a server, read the removals twice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Held Packages
&lt;/h3&gt;

&lt;p&gt;A held package is different from a package that is merely kept back. A hold is an explicit instruction that prevents APT from automatically installing, upgrading, or removing that package. Holds are useful for pinning a kernel, database, driver, or runtime version, and they are also easy to forget.&lt;/p&gt;

&lt;p&gt;List held packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-mark showhold
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hold a package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-mark hold package-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remove a hold:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-mark unhold package-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see dependency errors involving a held package, decide whether the hold is still intentional. Do not remove holds automatically. They may be protecting a production service, driver, or compatibility-sensitive toolchain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Broken Dependencies
&lt;/h3&gt;

&lt;p&gt;Broken dependencies mean APT cannot find a valid package set, which usually points to a version conflict rather than a corrupted download.&lt;/p&gt;

&lt;p&gt;Common causes include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A package requires a version that is not available.&lt;/li&gt;
&lt;li&gt;A PPA provides a newer library than Ubuntu expects.&lt;/li&gt;
&lt;li&gt;A manually installed &lt;code&gt;.deb&lt;/code&gt; depends on packages from another release.&lt;/li&gt;
&lt;li&gt;A package install was interrupted.&lt;/li&gt;
&lt;li&gt;A repository for the wrong Ubuntu release is enabled.&lt;/li&gt;
&lt;li&gt;Package pinning or holds prevent the needed version change.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dpkg &lt;span class="nt"&gt;--configure&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nt"&gt;--fix-broken&lt;/span&gt; &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then inspect the package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-cache policy package-name
apt-cache depends package-name
apt-cache rdepends package-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then use those commands to find the package version conflict that is blocking APT, rather than running every repair command in sequence.&lt;/p&gt;

&lt;h3&gt;
  
  
  GPG Key and NO_PUBKEY Errors
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;NO_PUBKEY&lt;/code&gt; error means APT received repository metadata, but it cannot verify the signature because the required public key is missing — a trust problem, not merely a download problem.&lt;/p&gt;

&lt;p&gt;A typical error looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The following signatures could not be verified because the public key is not available: NO_PUBKEY ABCD1234EF567890
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Old instructions often used &lt;code&gt;apt-key add&lt;/code&gt;, but avoid that for new repository setup. Modern Ubuntu systems should use a repository-specific keyring and &lt;code&gt;signed-by&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A better pattern looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 /etc/apt/keyrings

curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://example.com/repo-key.gpg &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sudo &lt;/span&gt;gpg &lt;span class="nt"&gt;--dearmor&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /etc/apt/keyrings/example.gpg

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"deb [signed-by=/etc/apt/keyrings/example.gpg] https://example.com/apt stable main"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/sources.list.d/example.list

&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace the URL and repository line with the official instructions from the vendor.&lt;/p&gt;

&lt;p&gt;The important part is this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;signed-by=/etc/apt/keyrings/example.gpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;signed-by&lt;/code&gt; line binds the key to one repository, which is cleaner and safer than putting every third-party key into a global trust store.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bad PPA or Release File Errors
&lt;/h3&gt;

&lt;p&gt;PPA failures often look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The repository does not have a Release file.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;404 Not Found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Common causes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The PPA does not support your Ubuntu release.&lt;/li&gt;
&lt;li&gt;You upgraded Ubuntu, but the PPA still points to the old codename.&lt;/li&gt;
&lt;li&gt;The PPA was deleted.&lt;/li&gt;
&lt;li&gt;The maintainer stopped publishing packages.&lt;/li&gt;
&lt;li&gt;You added a PPA intended for a different Ubuntu version.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check your codename:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;.&lt;/span&gt; /etc/os-release
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VERSION_CODENAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;List third-party source files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; /etc/apt/sources.list.d/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inspect them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="s2"&gt;"^deb"&lt;/span&gt; /etc/apt/sources.list /etc/apt/sources.list.d/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Disable a suspect source by renaming it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mv&lt;/span&gt; /etc/apt/sources.list.d/example.list /etc/apt/sources.list.d/example.list.disabled
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If APT works after disabling the file, you have found the problematic source and can fix or remove it permanently.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Safe APT Troubleshooting Workflow
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Refresh Metadata
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this fails, fix repositories before trying package repair. APT cannot solve dependencies correctly if its package index is stale or incomplete.&lt;/p&gt;

&lt;p&gt;Look for lines containing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NO_PUBKEY
Release file
404 Not Found
does not have a Release file
The repository is not signed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are repository or trust problems, and they should be fixed before you attempt any package repair.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Finish Interrupted Package Configuration
&lt;/h3&gt;

&lt;p&gt;If a package install was interrupted, &lt;code&gt;dpkg&lt;/code&gt; may have unpacked files but not configured the package.&lt;/p&gt;

&lt;p&gt;Run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dpkg &lt;span class="nt"&gt;--configure&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this succeeds, continue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nt"&gt;--fix-broken&lt;/span&gt; &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it fails, read the package name and error carefully. The package named at the bottom of the error is often more important than the package you originally tried to install.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Ask APT to Repair Dependencies
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nt"&gt;--fix-broken&lt;/span&gt; &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command asks APT to correct dependency problems by installing missing dependencies or removing packages that cannot be satisfied. Read the proposed action carefully, especially any removals.&lt;/p&gt;

&lt;p&gt;Be cautious if APT wants to remove:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ubuntu-desktop&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ubuntu-server&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;linux-generic&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;display manager packages&lt;/li&gt;
&lt;li&gt;network packages&lt;/li&gt;
&lt;li&gt;database packages&lt;/li&gt;
&lt;li&gt;container runtime packages&lt;/li&gt;
&lt;li&gt;desktop environment packages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sometimes removing a metapackage is harmless. Sometimes it is a warning sign that your package sources are mixed badly. Do not accept large removals without understanding them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Check Held Packages
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-mark showhold
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a held package is blocking the upgrade, inspect it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-cache policy package-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the hold is no longer needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-mark unhold package-name
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt upgrade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the hold is intentional, do not fight APT — fix the repository or package selection around the hold instead of removing the protection.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Inspect Package Versions
&lt;/h3&gt;

&lt;p&gt;For a package with dependency trouble:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-cache policy package-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Installed version&lt;/li&gt;
&lt;li&gt;Candidate version&lt;/li&gt;
&lt;li&gt;Available versions&lt;/li&gt;
&lt;li&gt;Which repository provides each version&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;apt-cache policy&lt;/code&gt; is one of the most useful APT debugging commands because it shows where each available version comes from.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-cache policy docker-ce
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the candidate version comes from an unexpected PPA or old repository, you have found the likely cause of the dependency conflict.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Look for Mixed Repositories
&lt;/h3&gt;

&lt;p&gt;List enabled sources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="s2"&gt;"^deb"&lt;/span&gt; /etc/apt/sources.list /etc/apt/sources.list.d/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Old Ubuntu codenames&lt;/li&gt;
&lt;li&gt;Debian repositories on Ubuntu&lt;/li&gt;
&lt;li&gt;PPAs for another Ubuntu release&lt;/li&gt;
&lt;li&gt;Duplicate vendor repositories&lt;/li&gt;
&lt;li&gt;Both Snap and APT instructions mixed for the same tool&lt;/li&gt;
&lt;li&gt;Old &lt;code&gt;.list&lt;/code&gt; files left behind after uninstalling software&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A common anti-pattern is installing a tool from a vendor repository, then later adding a PPA or manual &lt;code&gt;.deb&lt;/code&gt; that provides overlapping libraries. APT can handle many sources, but it cannot reconcile conflicting intentions unless you align the repositories yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7: Try a Simulated Install or Upgrade
&lt;/h3&gt;

&lt;p&gt;Before making changes, simulate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nb"&gt;install &lt;/span&gt;package-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt &lt;span class="nt"&gt;-s&lt;/span&gt; full-upgrade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-s&lt;/code&gt; option simulates the operation. It is useful when you want to see what APT would do without changing the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing Held Packages
&lt;/h2&gt;

&lt;h3&gt;
  
  
  List Held Packages
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-mark showhold
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If nothing is printed, no packages are held with &lt;code&gt;apt-mark&lt;/code&gt;, and you can move on to dependency or repository checks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hold a Package Intentionally
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-mark hold package-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Good reasons to hold a package:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A kernel version is known to work with your hardware.&lt;/li&gt;
&lt;li&gt;A database upgrade needs planning.&lt;/li&gt;
&lt;li&gt;A driver update breaks your GPU.&lt;/li&gt;
&lt;li&gt;A runtime version must match production.&lt;/li&gt;
&lt;li&gt;A vendor package is not compatible with the latest dependency.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bad reasons to hold a package:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You copied a command from an old guide.&lt;/li&gt;
&lt;li&gt;You forgot why it was held.&lt;/li&gt;
&lt;li&gt;You are avoiding a dependency error without understanding it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Remove a Hold
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-mark unhold package-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then update and upgrade:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt upgrade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the package still will not upgrade, it was not only a hold problem. Check policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-cache policy package-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Fixing Broken Dependencies
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Standard Repair Sequence
&lt;/h3&gt;

&lt;p&gt;Use this sequence when package installation or upgrade failed halfway:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;dpkg &lt;span class="nt"&gt;--configure&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nt"&gt;--fix-broken&lt;/span&gt; &lt;span class="nb"&gt;install
sudo &lt;/span&gt;apt upgrade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This order matters because each step prepares the ground for the next: &lt;code&gt;apt update&lt;/code&gt; refreshes repository metadata, &lt;code&gt;dpkg --configure -a&lt;/code&gt; finishes configuring unpacked packages, &lt;code&gt;apt --fix-broken install&lt;/code&gt; lets APT reconcile missing or conflicting dependencies, and &lt;code&gt;apt upgrade&lt;/code&gt; resumes normal package upgrades.&lt;/p&gt;

&lt;h3&gt;
  
  
  Remove a Badly Installed Local Package
&lt;/h3&gt;

&lt;p&gt;If the problem started after installing a downloaded &lt;code&gt;.deb&lt;/code&gt;, inspect it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dpkg &lt;span class="nt"&gt;-l&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;package-name
apt-cache policy package-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remove it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt remove package-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If configuration files are also causing problems:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt purge package-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then repair:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nt"&gt;--fix-broken&lt;/span&gt; &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Reinstall a Damaged Package
&lt;/h3&gt;

&lt;p&gt;If a package is installed but broken:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--reinstall&lt;/span&gt; package-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is useful when files are missing or corrupted, but the package sources are otherwise healthy and you want to refresh the installed files without changing versions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing PPA and Third-Party Repository Problems
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Find PPAs and Third-Party Repositories
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; /etc/apt/sources.list.d/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Show active repository lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="s2"&gt;"^deb"&lt;/span&gt; /etc/apt/sources.list /etc/apt/sources.list.d/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On newer Ubuntu systems, you may also see &lt;code&gt;.sources&lt;/code&gt; files using the deb822 format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; /etc/apt/sources.list.d/&lt;span class="k"&gt;*&lt;/span&gt;.sources
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Display them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/apt/sources.list.d/&lt;span class="k"&gt;*&lt;/span&gt;.sources
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Disable a PPA Safely
&lt;/h3&gt;

&lt;p&gt;Rename the source file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mv&lt;/span&gt; /etc/apt/sources.list.d/example.list /etc/apt/sources.list.d/example.list.disabled
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For deb822 files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mv&lt;/span&gt; /etc/apt/sources.list.d/example.sources /etc/apt/sources.list.d/example.sources.disabled
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Renaming the source file is reversible and safer than deleting repository configuration immediately, because you can rename it back if you disabled the wrong source.&lt;/p&gt;

&lt;h3&gt;
  
  
  Remove Packages from a PPA
&lt;/h3&gt;

&lt;p&gt;Disabling a PPA stops future package downloads from it. It does not automatically downgrade packages already installed from that PPA.&lt;/p&gt;

&lt;p&gt;If the PPA caused library conflicts, you may need to downgrade packages back to Ubuntu versions.&lt;/p&gt;

&lt;p&gt;Install &lt;code&gt;ppa-purge&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;ppa-purge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then purge the PPA:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ppa-purge ppa:owner/name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;ppa-purge&lt;/code&gt; carefully and read the proposed changes before accepting, because it may remove or downgrade several related packages.&lt;/p&gt;

&lt;h3&gt;
  
  
  After a Release Upgrade
&lt;/h3&gt;

&lt;p&gt;After upgrading Ubuntu, old PPAs are a common source of errors.&lt;/p&gt;

&lt;p&gt;Check for old codenames:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="s2"&gt;"jammy&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;noble&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;oracular&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;plucky"&lt;/span&gt; /etc/apt/sources.list /etc/apt/sources.list.d/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adjust the codenames for your real system. For example, if you are on Ubuntu 24.04, your codename is &lt;code&gt;noble&lt;/code&gt;. If a third-party source still points to an older codename, verify whether that vendor supports your current Ubuntu release. If you are setting up a new machine rather than repairing an upgrade, our &lt;a href="https://www.glukhov.org/developer-tools/local-dev-platforms/install-linux-ubuntu-24-04/" rel="noopener noreferrer"&gt;Ubuntu 24.04 install guide&lt;/a&gt; walks through adding vendor repositories with &lt;code&gt;signed-by&lt;/code&gt; from the start.&lt;/p&gt;

&lt;p&gt;Do not just edit the codename and hope for the best — some repositories do not publish packages for every Ubuntu version, so verify vendor support for your release first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing GPG and NO_PUBKEY Errors
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What NO_PUBKEY Means
&lt;/h3&gt;

&lt;p&gt;APT repositories publish signed metadata, and your machine needs the matching public key to verify that metadata. If the key is missing, APT refuses to trust the repository, which is the behavior you want — do not disable signature checks just to make the error disappear.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Modern Keyring Pattern
&lt;/h3&gt;

&lt;p&gt;Create the keyring directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 /etc/apt/keyrings
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Download and dearmor the vendor key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://example.com/repository-key.gpg &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sudo &lt;/span&gt;gpg &lt;span class="nt"&gt;--dearmor&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /etc/apt/keyrings/example.gpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set readable permissions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;0644 /etc/apt/keyrings/example.gpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the repository with &lt;code&gt;signed-by&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"deb [signed-by=/etc/apt/keyrings/example.gpg] https://example.com/linux/ubuntu noble stable"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/sources.list.d/example.list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then update:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;noble&lt;/code&gt; with your Ubuntu codename if needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;.&lt;/span&gt; /etc/os-release
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VERSION_CODENAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why apt-key Is the Wrong Habit
&lt;/h3&gt;

&lt;p&gt;Old guides often use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://example.com/key.gpg | &lt;span class="nb"&gt;sudo &lt;/span&gt;apt-key add -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Avoid &lt;code&gt;apt-key add&lt;/code&gt; for new setups. The old &lt;code&gt;apt-key&lt;/code&gt; style adds keys to a broad trust area, which makes it harder to reason about which key is trusted for which repository, whereas the modern &lt;code&gt;signed-by&lt;/code&gt; style scopes the key to a specific repository and is basic supply-chain hygiene.&lt;/p&gt;

&lt;h3&gt;
  
  
  Find Legacy Trusted Keys
&lt;/h3&gt;

&lt;p&gt;You may still have old keys in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/etc/apt/trusted.gpg
/etc/apt/trusted.gpg.d/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;List files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; /etc/apt/trusted.gpg.d/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not delete keys randomly — first map each key to a repository, then migrate one repository at a time to &lt;code&gt;/etc/apt/keyrings&lt;/code&gt; and &lt;code&gt;signed-by&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Common GPG Mistakes
&lt;/h3&gt;

&lt;p&gt;Do not use random keyservers as your first choice when fixing &lt;code&gt;NO_PUBKEY&lt;/code&gt; errors.&lt;/p&gt;

&lt;p&gt;Better order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use the vendor's official install documentation.&lt;/li&gt;
&lt;li&gt;Download the key from the vendor's official HTTPS URL.&lt;/li&gt;
&lt;li&gt;Store it in &lt;code&gt;/etc/apt/keyrings&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Bind it with &lt;code&gt;signed-by&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;sudo apt update&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Avoid these shortcuts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update &lt;span class="nt"&gt;--allow-unauthenticated&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--allow-unauthenticated&lt;/span&gt; package-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They may work temporarily, but they remove the signature verification that protects you from tampered repository metadata.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing "The Repository Is Not Signed"
&lt;/h2&gt;

&lt;p&gt;This error usually means one of these things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The repository does not publish signed metadata.&lt;/li&gt;
&lt;li&gt;The repository URL is wrong.&lt;/li&gt;
&lt;li&gt;The repository no longer supports your Ubuntu version.&lt;/li&gt;
&lt;li&gt;A proxy or mirror is returning the wrong content.&lt;/li&gt;
&lt;li&gt;You are using HTTP where the vendor now expects HTTPS.&lt;/li&gt;
&lt;li&gt;The source file has the wrong suite or component.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Find the failing source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;APT will print the URL. Then search for it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="s2"&gt;"example.com"&lt;/span&gt; /etc/apt/sources.list /etc/apt/sources.list.d/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Disable it temporarily:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mv&lt;/span&gt; /etc/apt/sources.list.d/example.list /etc/apt/sources.list.d/example.list.disabled
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If APT works again after disabling the file, reinstall that repository from the vendor's current official instructions rather than re-enabling the old configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing Duplicate Repository Warnings
&lt;/h2&gt;

&lt;p&gt;APT may warn that a target is configured multiple times.&lt;/p&gt;

&lt;p&gt;List matching entries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="s2"&gt;"repo-url-or-domain"&lt;/span&gt; /etc/apt/sources.list /etc/apt/sources.list.d/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Duplicate repositories often appear after running vendor install scripts multiple times.&lt;/p&gt;

&lt;p&gt;Keep one source file. Disable the others:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mv&lt;/span&gt; /etc/apt/sources.list.d/duplicate.list /etc/apt/sources.list.d/duplicate.list.disabled
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Duplicate warnings are not always fatal, but they are a sign of sloppy configuration, so keep one source file and disable the duplicates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing Packages from the Wrong Ubuntu Release
&lt;/h2&gt;

&lt;p&gt;One of the worst APT problems is mixing Ubuntu releases — for example, a machine on Ubuntu 24.04 should not casually pull packages from Ubuntu 22.04 or Debian testing. Sometimes it works for a while, but eventually the dependency graph becomes a puzzle that APT cannot solve cleanly.&lt;/p&gt;

&lt;p&gt;Check your release:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;.&lt;/span&gt; /etc/os-release
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VERSION_CODENAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Search sources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="s2"&gt;"^deb"&lt;/span&gt; /etc/apt/sources.list /etc/apt/sources.list.d/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for foreign codenames in the enabled sources, then inspect the affected package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-cache policy package-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the installed version came from an old or foreign repository, disable that repository and downgrade or reinstall the affected package from Ubuntu repositories.&lt;/p&gt;

&lt;p&gt;A conservative repair path is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--reinstall&lt;/span&gt; package-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For deeper conflicts, you may need to remove the package and reinstall it from the correct source rather than forcing an upgrade over a foreign version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleaning APT Cache and Unused Packages
&lt;/h2&gt;

&lt;p&gt;APT cache cleanup is not a dependency fix on its own, but it can help after many failed installs by reclaiming disk space and clearing stale package files.&lt;/p&gt;

&lt;p&gt;Remove packages that were installed automatically and are no longer needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt autoremove
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean downloaded package files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt clean
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or remove only obsolete package files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt autoclean
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;autoremove&lt;/code&gt; carefully on servers and desktops with manually installed driver stacks, and read the removal list before accepting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical APT Troubleshooting Recipes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Recipe: Package Is Kept Back
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
apt list &lt;span class="nt"&gt;--upgradable&lt;/span&gt;
apt-mark showhold
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt full-upgrade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If APT proposes reasonable changes after simulation, accept them. If it proposes large removals, stop and inspect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-cache policy package-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Recipe: Held Package Blocks Upgrade
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-mark showhold
apt-cache policy package-name
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-mark unhold package-name
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt upgrade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only unhold a package if the hold is no longer intentional, because removing a hold that protects production software can trigger a breaking upgrade.&lt;/p&gt;

&lt;h3&gt;
  
  
  Recipe: Interrupted Install
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dpkg &lt;span class="nt"&gt;--configure&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nt"&gt;--fix-broken&lt;/span&gt; &lt;span class="nb"&gt;install
sudo &lt;/span&gt;apt upgrade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Recipe: NO_PUBKEY Error
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Identify the repository from &lt;code&gt;sudo apt update&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Find the vendor's current official install instructions.&lt;/li&gt;
&lt;li&gt;Install the key into &lt;code&gt;/etc/apt/keyrings&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;signed-by&lt;/code&gt; in the source file.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;sudo apt update&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Example structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 /etc/apt/keyrings

curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://example.com/key.gpg &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sudo &lt;/span&gt;gpg &lt;span class="nt"&gt;--dearmor&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /etc/apt/keyrings/example.gpg

&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;0644 /etc/apt/keyrings/example.gpg

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"deb [signed-by=/etc/apt/keyrings/example.gpg] https://example.com/ubuntu noble main"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/sources.list.d/example.list

&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Recipe: PPA Does Not Have a Release File
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;ls&lt;/span&gt; /etc/apt/sources.list.d/
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="s2"&gt;"ppa.launchpadcontent.net&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;launchpad"&lt;/span&gt; /etc/apt/sources.list.d/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Disable the PPA:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mv&lt;/span&gt; /etc/apt/sources.list.d/example.list /etc/apt/sources.list.d/example.list.disabled
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then decide whether to remove, replace, or purge packages from that PPA.&lt;/p&gt;

&lt;h3&gt;
  
  
  Recipe: Manual &lt;code&gt;.deb&lt;/code&gt; Broke Dependencies
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dpkg &lt;span class="nt"&gt;-l&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;package-name
apt-cache policy package-name
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt remove package-name
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nt"&gt;--fix-broken&lt;/span&gt; &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you still need the software, prefer the vendor's current APT repository over repeated manual &lt;code&gt;.deb&lt;/code&gt; installs, which tend to accumulate dependency conflicts over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Essential APT Troubleshooting Commands
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Repository and Metadata
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="s2"&gt;"^deb"&lt;/span&gt; /etc/apt/sources.list /etc/apt/sources.list.d/
&lt;span class="nb"&gt;ls&lt;/span&gt; /etc/apt/sources.list.d/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Package State
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt list &lt;span class="nt"&gt;--installed&lt;/span&gt;
apt list &lt;span class="nt"&gt;--upgradable&lt;/span&gt;
apt-mark showhold
dpkg &lt;span class="nt"&gt;-l&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;package-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Package Policy and Dependencies
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-cache policy package-name
apt-cache depends package-name
apt-cache rdepends package-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Repair
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dpkg &lt;span class="nt"&gt;--configure&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nt"&gt;--fix-broken&lt;/span&gt; &lt;span class="nb"&gt;install
sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--reinstall&lt;/span&gt; package-name
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt full-upgrade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Cleanup
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt autoremove
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt autoclean
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt clean
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Simulation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nb"&gt;install &lt;/span&gt;package-name
apt &lt;span class="nt"&gt;-s&lt;/span&gt; full-upgrade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What Not To Do
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Do Not Randomly Delete &lt;code&gt;/var/lib/dpkg&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;If you see advice to delete dpkg state files, be skeptical. The dpkg database is the record of installed packages, and deleting pieces of it can turn a repairable package issue into a full system recovery project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do Not Disable Signature Verification
&lt;/h3&gt;

&lt;p&gt;Avoid:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nt"&gt;--allow-unauthenticated&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a repository cannot be verified, fix the key or disable the repository rather than bypassing authentication.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do Not Mix Ubuntu Releases Casually
&lt;/h3&gt;

&lt;p&gt;Do not add repositories for another Ubuntu release unless you understand APT pinning and the dependency consequences.&lt;/p&gt;

&lt;p&gt;This applies especially to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;desktop environments&lt;/li&gt;
&lt;li&gt;graphics drivers&lt;/li&gt;
&lt;li&gt;Python stacks&lt;/li&gt;
&lt;li&gt;container runtimes&lt;/li&gt;
&lt;li&gt;Kubernetes packages&lt;/li&gt;
&lt;li&gt;database packages&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Do Not Treat PPAs as Harmless
&lt;/h3&gt;

&lt;p&gt;PPAs are useful, but they are still package repositories that can replace libraries and system packages — which may be exactly what you want, or exactly why your next upgrade breaks. Prefer PPAs for specific applications, not for broad system foundations, unless you trust the maintainer and understand the upgrade path.&lt;/p&gt;

&lt;h2&gt;
  
  
  APT Troubleshooting Decision Tree
&lt;/h2&gt;

&lt;p&gt;Use this mental model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A["Does sudo apt update fail?"] --&amp;gt;|yes| B["Fix repositories, GPG keys, PPAs, network, or release files"]
    A --&amp;gt;|no| C["Did an install or upgrade get interrupted?"]
    C --&amp;gt;|yes| D["Run dpkg --configure -a, then apt --fix-broken install"]
    C --&amp;gt;|no| E["Are packages held?"]
    E --&amp;gt;|yes| F["Inspect apt-mark showhold and decide whether to unhold"]
    E --&amp;gt;|no| G["Are packages kept back?"]
    G --&amp;gt;|yes| H["Inspect apt full-upgrade simulation and package policy"]
    G --&amp;gt;|no| I["Is a third-party source involved?"]
    I --&amp;gt;|yes| J["Inspect apt-cache policy and source files"]
    I --&amp;gt;|no| K["Inspect the specific package dependency error"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most APT problems become manageable once you stop treating them as one big error and start separating repository health, package state, dependency solving, and trust configuration — the decision tree above is a shorthand for that discipline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommended Baseline for Developer Machines
&lt;/h2&gt;

&lt;p&gt;For a clean Ubuntu developer workstation, I prefer this baseline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep Ubuntu repositories standard.&lt;/li&gt;
&lt;li&gt;Use vendor APT repositories only when they are official and maintained.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;/etc/apt/keyrings&lt;/code&gt; and &lt;code&gt;signed-by&lt;/code&gt; for third-party repositories.&lt;/li&gt;
&lt;li&gt;Avoid old &lt;code&gt;apt-key&lt;/code&gt; instructions.&lt;/li&gt;
&lt;li&gt;Avoid mixing PPAs that replace core system libraries.&lt;/li&gt;
&lt;li&gt;Use containers, &lt;code&gt;uv&lt;/code&gt;, &lt;code&gt;pipx&lt;/code&gt;, &lt;code&gt;asdf&lt;/code&gt;, &lt;code&gt;mise&lt;/code&gt;, or language-native tools for fast-moving developer dependencies.&lt;/li&gt;
&lt;li&gt;Keep APT responsible for the operating system, drivers, services, and stable CLI tools.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For desktop software, prefer &lt;a href="https://www.glukhov.org/developer-tools/package-management/snap-vs-flatpack/" rel="noopener noreferrer"&gt;Flatpak or Snap&lt;/a&gt; over PPAs when a sandboxed universal package fits your needs. APT is excellent when it manages the base system, but it becomes painful when it is forced to behave like a universal developer dependency manager for fast-moving language ecosystems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final APT Troubleshooting Checklist
&lt;/h2&gt;

&lt;p&gt;When APT is broken on Ubuntu, work through this checklist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ ] Run sudo apt update and read the first real error.
[ ] Check Ubuntu codename with /etc/os-release.
[ ] Finish interrupted installs with dpkg --configure -a.
[ ] Repair dependencies with apt --fix-broken install.
[ ] Check held packages with apt-mark showhold.
[ ] Inspect package versions with apt-cache policy.
[ ] Disable broken PPAs or third-party repositories.
[ ] Replace apt-key style repositories with signed-by keyrings.
[ ] Simulate risky operations with apt -s.
[ ] Read removals before accepting full-upgrade or autoremove.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;APT is not fragile, but it is strict, and that strictness is a feature: it prevents unsigned repositories, impossible dependency sets, and accidental package replacements from silently changing your system. The calm way to fix APT is to preserve that strictness, find the conflicting state, and repair the smallest thing that is actually wrong.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>selfhosting</category>
    </item>
    <item>
      <title>Multi-Agent Orchestration Patterns: A Practical Guide</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Mon, 06 Jul 2026 11:25:31 +0000</pubDate>
      <link>https://dev.to/rosgluk/multi-agent-orchestration-patterns-a-practical-guide-2ap7</link>
      <guid>https://dev.to/rosgluk/multi-agent-orchestration-patterns-a-practical-guide-2ap7</guid>
      <description>&lt;p&gt;Single-agent AI systems peaked in 2025 — you gave one LLM a prompt, some tools, and a goal, and it did reasonably well on bounded tasks.&lt;/p&gt;

&lt;p&gt;In 2026, multi-agent systems have moved from research demos to production infrastructure. Gartner reports a 1,445% increase in multi-agent system inquiries from Q1 2024 to Q2 2025, while Salesforce's 2026 Connectivity Benchmark Report found organizations use an average of 12 agents, projected to grow 67% within two years. The &lt;a href="https://www.glukhov.org/ai-systems/" rel="noopener noreferrer"&gt;AI Systems cluster&lt;/a&gt; covers the full stack these systems operate on — from inference and memory to routing and observability.&lt;/p&gt;

&lt;p&gt;But here's what's less discussed: &lt;strong&gt;40% of multi-agent pilots fail within six months of production deployment&lt;/strong&gt;. The failure isn't that multi-agent systems don't work. The failure is that teams pick the wrong orchestration pattern for their problem — or pick the right one without understanding how it breaks.&lt;/p&gt;

&lt;p&gt;This guide covers the orchestration patterns that hold up in production, the specific ways each one fails, and a decision framework for picking the right architecture.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Core Problem: Coordination Is Hard
&lt;/h2&gt;

&lt;p&gt;When you move from a single AI agent to multiple agents working together, the first engineering question is: &lt;strong&gt;how do they coordinate?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The coordination model — the orchestration pattern — determines your system's latency, fault tolerance, scalability ceiling, and debugging complexity. It is consistently the highest-impact architectural decision in multi-agent design, conditioning every subsequent implementation choice.&lt;/p&gt;

&lt;p&gt;Every production multi-agent system maps to one of six canonical patterns, or a hybrid of two or more. The patterns emerge from distributed systems constraints: coordination cost, fault isolation, throughput requirements, and observability.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pattern 1: Orchestrator-Worker
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How It Works
&lt;/h3&gt;

&lt;p&gt;Orchestrator-Worker is the &lt;strong&gt;centralized hub-and-spoke&lt;/strong&gt; model of multi-agent coordination. A single orchestrator agent receives the task, decomposes it into subtasks, delegates each subtask to a specialist worker agent, and aggregates the results. Workers do not communicate directly with each other — all coordination flows through the orchestrator, which holds the full plan and decision-making authority.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;graph TD
    O[Orchestrator&amp;lt;br/&amp;gt;planner] --&amp;gt; WA[Worker A]
    O --&amp;gt; WB[Worker B]
    O --&amp;gt; WC[Worker C]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  When to Use It
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Cross-functional workflows with clear task decomposition&lt;/li&gt;
&lt;li&gt;Triage and routing scenarios (customer support, incident classification)&lt;/li&gt;
&lt;li&gt;Workloads where a single accountability point is required&lt;/li&gt;
&lt;li&gt;Tasks where the orchestrator can use a capable model while workers use cheaper, task-specific models&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Real-world example:&lt;/strong&gt; Salesforce Agentforce 2.0 uses orchestrator-worker to decompose customer inquiries into research, draft, and review stages.&lt;/p&gt;

&lt;h3&gt;
  
  
  How It Fails
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Single point of failure.&lt;/strong&gt; The orchestrator is both a bottleneck and a failure point. If the orchestrator's LLM call takes 3 seconds and you have 20 workers waiting for assignments, your decomposition throughput ceiling is roughly 6.7 tasks per second. If the orchestrator misclassifies a task, the wrong worker gets it — and misclassification rates compound at scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context overflow.&lt;/strong&gt; The orchestrator accumulates context from all workers. At 4+ workers, the orchestrator frequently exceeds context limits because it holds the full conversation history for every worker interaction simultaneously.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost explosion.&lt;/strong&gt; Workflows that cost $0.50 in testing can hit $50,000/month at 100K executions. The orchestrator makes multiple LLM calls for decomposition and aggregation on top of every worker call. At scale, the overhead dominates the worker cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mitigations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Set explicit interface contracts between orchestrator and workers&lt;/li&gt;
&lt;li&gt;Require structured outputs from workers (JSON schemas, typed responses)&lt;/li&gt;
&lt;li&gt;Bound sub-task budgets (token limits, step limits) to prevent runaway costs&lt;/li&gt;
&lt;li&gt;Consider a hierarchical variant (see Pattern 4) when worker count exceeds 5&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Pattern 2: Sequential Pipeline
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How It Works
&lt;/h3&gt;

&lt;p&gt;Sequential Pipeline is the &lt;strong&gt;linear chain with shared state&lt;/strong&gt; — a predefined sequence of agents with deterministic order, where each stage transforms or enriches data and passes it to the next. There is no runtime branching; the execution order is fixed at design time, making the pattern highly predictable but inflexible.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;graph LR
    I[Input] --&amp;gt; A1[Agent 1&amp;lt;br/&amp;gt;stage A]
    A1 --&amp;gt; A2[Agent 2&amp;lt;br/&amp;gt;stage B]
    A2 --&amp;gt; A3[Agent 3&amp;lt;br/&amp;gt;stage C]
    A3 --&amp;gt; O[Output]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  When to Use It
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Document processing workflows (ingest → extract → validate → output)&lt;/li&gt;
&lt;li&gt;Content generation pipelines (research → draft → edit → publish)&lt;/li&gt;
&lt;li&gt;Compliance verification (generate → check → revise → approve)&lt;/li&gt;
&lt;li&gt;Data enrichment and ETL workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Real-world example:&lt;/strong&gt; Microsoft Azure law firm workflow uses sequential pipelines for contract generation: draft → review → redline → final.&lt;/p&gt;

&lt;h3&gt;
  
  
  How It Fails
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Error propagation.&lt;/strong&gt; Bad output in stage 1 cascades downstream with no backtracking. A hallucination in the research stage produces a flawed draft, which the editor polishes into a confident but incorrect final output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Coordination overhead.&lt;/strong&gt; A 4-agent pipeline adds approximately 950ms of coordination overhead versus 500ms of processing time. You're paying 3x for the same result if specialization isn't required. Token consumption compounds: 29,000 tokens across a 4-agent pipeline versus 10,000 for a single agent doing the same work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No conditional branching.&lt;/strong&gt; The pipeline cannot adapt based on intermediate results. If stage 2 discovers the input is malformed, it has no mechanism to signal stage 1 to retry — it must either fail or produce degraded output.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mitigations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Insert quality gates between stages (lightweight validation agents that check output before passing downstream)&lt;/li&gt;
&lt;li&gt;Add reprocessing loops for stages that can retry — durable workflow engines such as &lt;a href="https://www.glukhov.org/app-architecture/integration-patterns/workflow-applications-temporal-in-go/" rel="noopener noreferrer"&gt;Temporal&lt;/a&gt; handle retry semantics reliably&lt;/li&gt;
&lt;li&gt;Keep pipelines to 3-4 stages maximum; beyond that, consider orchestrator-worker for conditional branching&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Pattern 3: Fan-Out / Fan-In
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How It Works
&lt;/h3&gt;

&lt;p&gt;Fan-Out / Fan-In is &lt;strong&gt;parallel execution with aggregation&lt;/strong&gt;. A dispatcher routes work to multiple agents running simultaneously, then a collector aggregates their results via voting, weighted merging, or LLM synthesis. Agents operate independently throughout execution and do not communicate with each other — the only shared boundary is the collector.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;graph TD
    D[Dispatcher] --&amp;gt; AA[Agent A]
    D --&amp;gt; AB[Agent B]
    D --&amp;gt; AC[Agent C]
    AA --&amp;gt; C[Collector&amp;lt;br/&amp;gt;merge]
    AB --&amp;gt; C
    AC --&amp;gt; C
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  When to Use It
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Multi-perspective analysis where diverse viewpoints are valuable&lt;/li&gt;
&lt;li&gt;Concurrent code review (multiple reviewers in parallel)&lt;/li&gt;
&lt;li&gt;4+ independent tasks that can be decomposed upfront&lt;/li&gt;
&lt;li&gt;Workloads where wall-clock time matters more than token efficiency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key metric:&lt;/strong&gt; Fan-out cuts wall-clock time by 75% compared to sequential execution. Four agents running in parallel complete in the time of one.&lt;/p&gt;

&lt;h3&gt;
  
  
  How It Fails
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;API rate limits.&lt;/strong&gt; Collective load exceeds capacity even if individual agents stay within limits. Five agents each making 10 requests per minute may exceed a 40 RPM limit that a single agent respects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quadratic race conditions.&lt;/strong&gt; Shared state conflicts scale as N(N-1)/2. With 5 agents, that's 10 potential conflicts. With 10 agents, it's 45. State management becomes the dominant complexity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Aggregation hallucination.&lt;/strong&gt; LLM synthesis can invent consensus. If Agent A says "yes" and Agent B says "no," the aggregator might produce "maybe" — a hallucinated middle ground that neither agent suggested. Requires explicit conflict resolution, not just summarization.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mitigations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Use explicit voting mechanisms rather than freeform synthesis&lt;/li&gt;
&lt;li&gt;Implement rate limiting at the dispatcher level&lt;/li&gt;
&lt;li&gt;Maintain separate state per worker; merge at the collector&lt;/li&gt;
&lt;li&gt;Set a maximum agent count (5-8) to keep race conditions manageable&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Pattern 4: Hierarchical
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How It Works
&lt;/h3&gt;

&lt;p&gt;Hierarchical is &lt;strong&gt;tree-structured delegation with multiple levels&lt;/strong&gt; — a top-level manager delegates to mid-level supervisors, which delegate to leaf-level workers. Each level adds a layer of abstraction: strategy at the top, tactics in the middle, and execution at the leaves. Context windows are managed at each level independently, so no single agent needs to hold the entire problem in context.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;graph TD
    TM[Top Manager] --&amp;gt; SA[Supervisor A]
    TM --&amp;gt; SB[Supervisor B]
    TM --&amp;gt; SC[Supervisor C]
    SA --&amp;gt; W1[Worker 1]
    SB --&amp;gt; W2[Worker 2]
    SC --&amp;gt; W3[Worker 3]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  When to Use It
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Complex multi-domain enterprise tasks requiring 20+ agents&lt;/li&gt;
&lt;li&gt;Large-scale codebase auditing where different modules need different specialists&lt;/li&gt;
&lt;li&gt;Massive document processing (thousands of documents across multiple categories)&lt;/li&gt;
&lt;li&gt;Tasks where no single agent's context window can hold the full problem&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key advantage:&lt;/strong&gt; Hierarchical systems scale logarithmically. Each manager handles a bounded number of subordinates, so adding workers doesn't linearly increase coordination overhead.&lt;/p&gt;

&lt;h3&gt;
  
  
  How It Fails
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Latency accumulation.&lt;/strong&gt; Each level adds latency. A 3-level hierarchy requires at least 6-12 seconds minimum, accumulating per level. The top manager waits for all supervisors, who wait for all workers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Information loss.&lt;/strong&gt; Summarization between levels is lossy. A supervisor summarizes worker output for the top manager, losing details that might be critical for the final decision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Branch failure isolation.&lt;/strong&gt; A failure in one branch doesn't propagate to others — which is good for fault tolerance but bad for consistency. Different branches might reach contradictory conclusions that the top manager cannot resolve.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mitigations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Set explicit summarization requirements for each level&lt;/li&gt;
&lt;li&gt;Implement cross-branch validation at the top manager&lt;/li&gt;
&lt;li&gt;Keep hierarchy depth to 2-3 levels maximum&lt;/li&gt;
&lt;li&gt;Use structured outputs at every level to reduce information loss&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Pattern 5: Swarm
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How It Works
&lt;/h3&gt;

&lt;p&gt;Swarm is &lt;strong&gt;decentralized emergent coordination with no central authority&lt;/strong&gt;. Autonomous agents make local decisions based on shared state (a blackboard) or environment signals, with no orchestrator directing the flow. Agents discover available tasks, claim them, and publish results back to the shared space. Coordination is emergent — the system self-organizes around available work, similar to how bees navigate to a new hive without a central coordinator.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;graph TB
    SB[Shared Blackboard&amp;lt;br/&amp;gt;tasks · results · observations]
    AA[Agent A] &amp;lt;--&amp;gt; SB
    AB[Agent B] &amp;lt;--&amp;gt; SB
    AC[Agent C] &amp;lt;--&amp;gt; SB
    AD[Agent D] &amp;lt;--&amp;gt; SB
    AE[Agent E] &amp;lt;--&amp;gt; SB
    AF[Agent F] &amp;lt;--&amp;gt; SB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  When to Use It
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Research flows where the optimal search path is unknown&lt;/li&gt;
&lt;li&gt;Competitive intelligence gathering across multiple sources&lt;/li&gt;
&lt;li&gt;Large-scale web scraping with dynamic target discovery&lt;/li&gt;
&lt;li&gt;Parallel hypothesis exploration in scientific or analytical domains&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key advantage:&lt;/strong&gt; A swarm of 50 research agents can explore 50 hypotheses in parallel without any central coordinator planning the search. The system self-organizes around available work.&lt;/p&gt;

&lt;h3&gt;
  
  
  How It Fails
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Debugging nightmare.&lt;/strong&gt; Without a central control flow, tracing failures requires distributed tracing and blackboard replay. You cannot follow a single execution path — you must reconstruct the emergent behavior from logs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No transactional guarantees.&lt;/strong&gt; Swarm patterns cannot enforce strict ordering or transactional consistency. If you need Agent A to complete before Agent B starts, a swarm is the wrong pattern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Termination conditions.&lt;/strong&gt; How does the swarm know when to stop? Without explicit termination criteria, agents may continue indefinitely, consuming compute and generating diminishing returns.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mitigations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Implement explicit termination conditions (time-based, result-count-based, or convergence-based)&lt;/li&gt;
&lt;li&gt;Use a blackboard with versioned entries to track state changes&lt;/li&gt;
&lt;li&gt;Add a monitoring agent that observes swarm behavior and can intervene&lt;/li&gt;
&lt;li&gt;Set agent-level budgets (maximum steps, maximum tokens) to prevent runaway execution — &lt;a href="https://www.glukhov.org/ai-systems/hermes/kanban-in-hermes/" rel="noopener noreferrer"&gt;Kanban-style dispatchers&lt;/a&gt; provide practical rate-limit and concurrency patterns for self-hosted swarm deployments&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Pattern 6: Mesh
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How It Works
&lt;/h3&gt;

&lt;p&gt;Mesh is &lt;strong&gt;direct peer-to-peer communication with persistent connections&lt;/strong&gt; — agents communicate with each other through explicit, predefined channels rather than through any central hub. The communication graph is typically defined at deployment time, so Agent A knows it needs Agent B for database queries and Agent C for authentication logic. For cross-team or cross-system agent communication, the &lt;a href="https://www.glukhov.org/ai-systems/mcp/a2a-vs-mcp-ai-agent-protocols/" rel="noopener noreferrer"&gt;A2A protocol&lt;/a&gt; provides a standardized discovery and messaging layer for mesh participants that span different frameworks or ownership boundaries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;graph LR
    A[Agent A] --- B[Agent B]
    A --- C[Agent C]
    B --- C
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  When to Use It
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Collaborative reasoning where agents need to share intermediate state&lt;/li&gt;
&lt;li&gt;Multi-agent coding systems (planner ↔ coder ↔ tester loops)&lt;/li&gt;
&lt;li&gt;Iterative artifact refinement where multiple specialists contribute&lt;/li&gt;
&lt;li&gt;Negotiation scenarios where agents represent different stakeholders&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key advantage:&lt;/strong&gt; Ideal for iterative refinement. Agents can pass partial results back and forth, building on each other's work without a central aggregator.&lt;/p&gt;

&lt;h3&gt;
  
  
  How It Fails
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Combinatorial explosion.&lt;/strong&gt; Connection count scales as N(N-1)/2. With 3 agents, that's 3 connections. With 8 agents, it's 28. Best limited to 3-8 tightly coupled agents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Circular dependencies.&lt;/strong&gt; Agent A calls Agent B, which calls Agent C, which calls Agent A. Without cycle detection, mesh patterns can enter infinite loops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Debugging complexity.&lt;/strong&gt; Non-deterministic routing makes tracing failures nearly impossible. When the output is wrong, you need to reconstruct which agents communicated with which, in what order.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mitigations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Define the communication graph at deployment time (not runtime)&lt;/li&gt;
&lt;li&gt;Implement cycle detection with maximum hop limits&lt;/li&gt;
&lt;li&gt;Use message passing with explicit acknowledgment&lt;/li&gt;
&lt;li&gt;Add a circuit breaker that terminates communication chains after N hops&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Decision Framework
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Start with the simplest pattern that fits your problem. Most teams over-architect toward multi-agent topologies long before the single-agent approach has been genuinely exhausted.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 1: Characterize Your Problem
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem Characteristic&lt;/th&gt;
&lt;th&gt;Recommended Pattern&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Known task decomposition, clear specialists&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Orchestrator-Worker&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixed sequence, no branching needed&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Sequential Pipeline&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Independent subtasks, need parallelism&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Fan-Out / Fan-In&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Complex, multi-domain, 20+ agents&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Hierarchical&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exploration, unknown search space&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Swarm&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Collaborative refinement, peer communication&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Mesh&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Step 2: Estimate Your Constraints
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Constraint&lt;/th&gt;
&lt;th&gt;Pattern to Avoid&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Low latency (&amp;lt; 2 seconds)&lt;/td&gt;
&lt;td&gt;Hierarchical, Mesh&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strict ordering required&lt;/td&gt;
&lt;td&gt;Swarm, Fan-Out&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single point of accountability&lt;/td&gt;
&lt;td&gt;Swarm, Mesh&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High fault tolerance needed&lt;/td&gt;
&lt;td&gt;Orchestrator-Worker, Sequential&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Budget-constrained&lt;/td&gt;
&lt;td&gt;Fan-Out (parallel = more tokens)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Complex debugging required&lt;/td&gt;
&lt;td&gt;Swarm, Mesh&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Step 3: Start Single-Agent
&lt;/h3&gt;

&lt;p&gt;The canonical agent loop — a single agent with tools, reasoning, and iteration — is still the right default for general-purpose agents. &lt;a href="https://www.glukhov.org/ai-systems/architecture/ai-assistant-architecture/" rel="noopener noreferrer"&gt;AI Assistant Architecture&lt;/a&gt; covers the five-layer foundation that single-agent systems build on, and it is worth mastering that foundation before layering in multi-agent coordination. Note that multi-agent systems are also fundamentally different from multi-model routing; for the latter, see &lt;a href="https://www.glukhov.org/llm-architecture/model-routing/multi-model-system-design/" rel="noopener noreferrer"&gt;Multi-Model System Design&lt;/a&gt;, which covers sequential, parallel, and ensemble patterns applied to model selection rather than agent coordination.&lt;/p&gt;

&lt;p&gt;Escalate to multi-agent only when measurement says you must:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Single agent context window is insufficient&lt;/li&gt;
&lt;li&gt;Task requires genuine parallelism (wall-clock time matters)&lt;/li&gt;
&lt;li&gt;Specialization provides measurable quality improvement&lt;/li&gt;
&lt;li&gt;Cost of single-agent approach exceeds multi-agent overhead&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For background and proactive agent work — scheduling, queue-based execution, durable polling loops — see &lt;a href="https://www.glukhov.org/ai-systems/architecture/polling-agents-ai-assistants-implementation-patterns/" rel="noopener noreferrer"&gt;Polling Agents in AI Assistants: 11 Implementation Patterns&lt;/a&gt;, which complements multi-agent orchestration patterns with the scheduling layer underneath them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Failure Modes: The MAST Taxonomy
&lt;/h2&gt;

&lt;p&gt;Research from NeurIPS 2025 (MAST — Multi-Agent System Failure Taxonomy) analyzed 1,600+ execution traces across seven popular multi-agent frameworks. Failures distribute across three root categories:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Specification Ambiguity (33% of failures)
&lt;/h3&gt;

&lt;p&gt;Agents misinterpret roles, duplicate work, or skip verification because their instructions are underspecified.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Use specification schemas. Define explicit role descriptions, task boundaries, and output formats for every agent. Structured schemas (JSON, Pydantic models) beat natural language instructions.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Coordination Breakdowns (33% of failures)
&lt;/h3&gt;

&lt;p&gt;Agents communicate using unstructured protocols, leading to message loss, race conditions, and circular handoffs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Implement structured coordination protocols. Use typed message passing, acknowledgment mechanisms, and explicit termination conditions.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Verification Gaps (33% of failures)
&lt;/h3&gt;

&lt;p&gt;No independent validation of agent outputs. Agents trust each other's output without verification, allowing errors to propagate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Add independent validation agents. Use a separate model or verification step to validate outputs before accepting them. This is the maker-checker pattern.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cost Control: The Hidden Multiplier
&lt;/h2&gt;

&lt;p&gt;Multi-agent systems have a cost structure that scales non-linearly:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;Cost Multiplier (vs single agent)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Orchestrator-Worker&lt;/td&gt;
&lt;td&gt;2-3x (orchestrator + workers)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sequential Pipeline&lt;/td&gt;
&lt;td&gt;3-4x (each stage pays full token cost)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fan-Out / Fan-In&lt;/td&gt;
&lt;td&gt;4-5x (all agents run fully)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hierarchical&lt;/td&gt;
&lt;td&gt;3-5x (depends on depth)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Swarm&lt;/td&gt;
&lt;td&gt;2-10x (depends on convergence)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mesh&lt;/td&gt;
&lt;td&gt;3-6x (depends on iteration count)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Cost optimization strategies:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Use cheaper models for workers.&lt;/strong&gt; The orchestrator needs reasoning capability; workers can use smaller, faster models.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bound execution budgets.&lt;/strong&gt; Set maximum tokens, maximum steps, and maximum time per agent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implement early termination.&lt;/strong&gt; Stop agents that have clearly failed or succeeded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache shared context.&lt;/strong&gt; Use prefix caching (vLLM, SGLang RadixAttention) to avoid recomputing shared system prompts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor per-agent cost.&lt;/strong&gt; Track token consumption per agent, not just total cost. Identify the most expensive agents and optimize first.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a deeper treatment of token optimization strategies — prompt compression, caching, batching, and smart model selection — see &lt;a href="https://www.glukhov.org/llm-performance/cost-effective-llm-applications/" rel="noopener noreferrer"&gt;Reduce LLM Costs: Token Optimization Strategies&lt;/a&gt;. The techniques apply equally to individual agent calls within a multi-agent system.&lt;/p&gt;




&lt;h2&gt;
  
  
  Observability: Seeing Inside the Black Box
&lt;/h2&gt;

&lt;p&gt;Multi-agent systems fail in ways that make traditional debugging inadequate. When multiple agents coordinate, issues propagate across agent boundaries, execution paths become unpredictable, and identifying root causes requires visibility into distributed workflows. &lt;a href="https://www.glukhov.org/observability/observability-for-llm-systems/" rel="noopener noreferrer"&gt;Observability for LLM Systems&lt;/a&gt; covers the full production observability stack — metrics, distributed tracing, logs, SLOs, and tool comparisons — that multi-agent systems rely on. For instrumenting vLLM and llama.cpp inference endpoints with Prometheus and Grafana, see &lt;a href="https://www.glukhov.org/observability/monitoring-llm-inference-prometheus-grafana/" rel="noopener noreferrer"&gt;Monitor LLM Inference in Production&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Essential Observability Components
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Distributed Tracing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Capture the complete interaction graph across all agents. Traditional tools show you whether components are running, but multi-agent debugging requires understanding how components interact and where coordination breaks down.&lt;/p&gt;

&lt;p&gt;Key spans to trace:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Orchestrator decomposition step&lt;/li&gt;
&lt;li&gt;Each worker's execution&lt;/li&gt;
&lt;li&gt;Aggregation step&lt;/li&gt;
&lt;li&gt;Cross-agent communication (mesh/swarm)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Blackboard Replay&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For swarm and mesh patterns, maintain a versioned blackboard that can be replayed. This allows you to reconstruct the emergent behavior that led to a failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Cost Attribution&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Track token consumption per agent, per step. Identify which agents are consuming disproportionate resources.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Convergence Monitoring&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For swarm and mesh patterns, monitor whether the system is converging or diverging. Set alerts for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Agent count exceeding expected bounds&lt;/li&gt;
&lt;li&gt;Iteration count exceeding thresholds&lt;/li&gt;
&lt;li&gt;Output quality degrading over time&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Framework Support Matrix
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;LangGraph&lt;/th&gt;
&lt;th&gt;AutoGen&lt;/th&gt;
&lt;th&gt;CrewAI&lt;/th&gt;
&lt;th&gt;OpenAI Agents SDK&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Orchestrator-Worker&lt;/td&gt;
&lt;td&gt;✅ Native&lt;/td&gt;
&lt;td&gt;✅ Native&lt;/td&gt;
&lt;td&gt;✅ Native&lt;/td&gt;
&lt;td&gt;✅ Native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sequential Pipeline&lt;/td&gt;
&lt;td&gt;✅ Graph edges&lt;/td&gt;
&lt;td&gt;✅ Sequential&lt;/td&gt;
&lt;td&gt;✅ Agent chains&lt;/td&gt;
&lt;td&gt;✅ Handoff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fan-Out / Fan-In&lt;/td&gt;
&lt;td&gt;✅ Superstep&lt;/td&gt;
&lt;td&gt;✅ Group chat&lt;/td&gt;
&lt;td&gt;✅ Crew&lt;/td&gt;
&lt;td&gt;✅ Parallel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hierarchical&lt;/td&gt;
&lt;td&gt;✅ Nested graphs&lt;/td&gt;
&lt;td&gt;✅ Hierarchical&lt;/td&gt;
&lt;td&gt;❌ Limited&lt;/td&gt;
&lt;td&gt;❌ Limited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Swarm&lt;/td&gt;
&lt;td&gt;❌ Limited&lt;/td&gt;
&lt;td&gt;✅ Swarm&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mesh&lt;/td&gt;
&lt;td&gt;✅ Custom graph&lt;/td&gt;
&lt;td&gt;✅ Group chat&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Putting It Together: A Production Example
&lt;/h2&gt;

&lt;p&gt;Real-world systems rarely map cleanly to a single pattern — most production deployments combine two or three approaches, each handling the part of the workflow it is best suited for. Infrastructure patterns like &lt;a href="https://www.glukhov.org/app-architecture/integration-patterns/go-microservices-for-ai-ml-orchestration-patterns/" rel="noopener noreferrer"&gt;Go Microservices for AI/ML Orchestration&lt;/a&gt; describe the service-level choreography and saga patterns that underpin these hybrid architectures at scale.&lt;/p&gt;

&lt;p&gt;Consider a customer support system that handles technical inquiries:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Triage (Orchestrator-Worker):&lt;/strong&gt; Incoming ticket → orchestrator classifies → routes to specialist&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Research (Fan-Out):&lt;/strong&gt; Specialist agent runs parallel queries (knowledge base, ticket history, product docs)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Draft (Sequential):&lt;/strong&gt; Research → draft response → quality check&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Escalation (Hierarchical):&lt;/strong&gt; If quality check fails, escalate to senior agent → human review&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This hybrid approach uses four patterns because no single pattern handles the full workflow optimally. The key insight: &lt;strong&gt;compose patterns, don't force one pattern to do everything.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Start simple.&lt;/strong&gt; Single-agent with tools is the default. Escalate to multi-agent only when measurement demands it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Match pattern to problem.&lt;/strong&gt; Orchestrator-worker for decomposition, pipeline for fixed sequences, fan-out for parallelism, hierarchical for scale, swarm for exploration, mesh for collaboration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expect failure modes.&lt;/strong&gt; Every pattern has specific ways it breaks. Design mitigations before you deploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost scales non-linearly.&lt;/strong&gt; Multi-agent systems multiply token consumption. Budget for 2-5x the cost of a single agent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability is non-negotiable.&lt;/strong&gt; Without distributed tracing and cost attribution, you cannot debug or optimize multi-agent systems.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compose patterns.&lt;/strong&gt; Most production systems use 2-3 patterns combined. Don't force a single pattern to handle everything.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The multi-agent landscape is maturing rapidly. The teams that succeed are those that understand the tradeoffs, pick patterns deliberately, and build observability from day one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is multi-agent orchestration?&lt;/strong&gt;&lt;br&gt;
Multi-agent orchestration is the coordination model that governs how multiple AI agents work together on a task. The pattern you choose — hub-and-spoke, pipeline, fan-out, hierarchical, swarm, or mesh — determines your system's latency, fault tolerance, scalability ceiling, and debugging complexity. Each pattern makes different tradeoffs and breaks in different ways.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which multi-agent pattern is best for production AI systems?&lt;/strong&gt;&lt;br&gt;
Most production systems start with orchestrator-worker. It provides clear accountability, debuggable control flow, and predictable costs. Escalate to hierarchical when worker count exceeds 5-8 and to fan-out when independent parallel tasks dominate the workload. Swarm and mesh remain niche patterns reserved for exploration workflows and tight peer collaboration respectively.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why do 40% of multi-agent pilots fail?&lt;/strong&gt;&lt;br&gt;
The three root causes according to the MAST taxonomy from NeurIPS 2025 are specification ambiguity (agents misinterpret roles or skip verification steps), coordination breakdowns (unstructured messaging leads to message loss and circular handoffs), and verification gaps (no independent validation of agent outputs, allowing errors to propagate unchecked). Each category accounts for roughly a third of all failures across 1,600+ analyzed execution traces.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much more does a multi-agent system cost than a single agent?&lt;/strong&gt;&lt;br&gt;
Expect 2 to 10 times the token cost depending on the pattern. Orchestrator-worker is cheapest at 2-3x. Fan-out and swarm are most expensive at 4-10x because agents run in parallel and each consumes a full token budget independently. These multipliers compound at scale — a workflow costing $0.50 in testing can reach $50,000 per month at 100K executions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you debug a multi-agent system when something goes wrong?&lt;/strong&gt;&lt;br&gt;
Start with distributed tracing — one trace per execution, with spans for each agent call, tool invocation, and aggregation step. For swarm and mesh patterns, implement blackboard replay so you can reconstruct the emergent behavior from logs. Per-agent cost attribution helps identify which agents are triggering cascading failures or runaway spend before they reach production scale.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>aicoding</category>
      <category>dev</category>
    </item>
    <item>
      <title>Speculative Decoding: 20-50% Faster LLM Inference</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Mon, 06 Jul 2026 07:11:15 +0000</pubDate>
      <link>https://dev.to/rosgluk/speculative-decoding-20-50-faster-llm-inference-g11</link>
      <guid>https://dev.to/rosgluk/speculative-decoding-20-50-faster-llm-inference-g11</guid>
      <description>&lt;p&gt;A 70B model generates one token per forward pass, and each pass reloads weights from VRAM, computes attention across the context, and synchronizes memory. Between tokens, the GPU sits idle while it waits for sequential dependencies to resolve.&lt;/p&gt;

&lt;p&gt;On an H100, a 70B model produces one token every 30-50ms. The GPU has enough compute capacity to process multiple tokens in parallel, but the sequential dependency prevents it — each token depends on the previous one, and the pipeline stalls.&lt;/p&gt;

&lt;p&gt;Speculative decoding breaks that bottleneck by letting you generate multiple tokens in the time it normally takes to generate one, without changing the output distribution. The tokens you get are statistically identical to what you'd get from standard autoregressive decoding; the only difference is how fast you get them.&lt;/p&gt;

&lt;p&gt;This guide covers the mechanics, the variants available in 2026, acceptance rate tradeoffs, and practical setup across llama.cpp, vLLM, SGLang, and TensorRT-LLM.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Autoregressive Decoding Works (and Why It's Slow)
&lt;/h2&gt;

&lt;p&gt;Before you can understand speculative decoding, you need to understand the autoregressive constraint it works around. Standard autoregressive generation processes tokens sequentially:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run a forward pass through the model with the current context.&lt;/li&gt;
&lt;li&gt;Sample the next token from the output distribution.&lt;/li&gt;
&lt;li&gt;Append the token to the context.&lt;/li&gt;
&lt;li&gt;Repeat.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each step requires a full forward pass — loading weights from VRAM, computing attention across the entire context, and producing a single token. For a model with 70B parameters, this takes roughly 30-50ms per token on an H100. The GPU has compute capacity to spare — it could process more work in parallel — but the sequential dependency prevents it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Compute-VRAM Gap
&lt;/h3&gt;

&lt;p&gt;Modern GPUs have more FLOPs than they need for single-token generation, so the real bottleneck is memory bandwidth — weights must be streamed from VRAM to the compute units for each forward pass. When generating one token at a time, the GPU spends most of its time waiting for memory transfers rather than doing useful compute.&lt;/p&gt;

&lt;p&gt;Speculative decoding addresses this by giving the GPU more work per memory transfer. Instead of one token per forward pass, it generates K tokens per forward pass, amortizing the memory cost across multiple outputs.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Draft-Verify Mechanism
&lt;/h2&gt;

&lt;p&gt;Speculative decoding works in repeating draft-verify cycles. A fast draft mechanism proposes K candidate tokens — from a small draft model, an n-gram lookup, or a prediction head attached to the target model — and the target model verifies all K in a single forward pass. The draft phase is cheap, typically 5-20% of the target model's forward pass time, while verification compares each drafted token against what the target would have generated, accepting the longest matching prefix and resampling from the first rejection onward.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sequenceDiagram
    participant Draft as Draft mechanism
    participant Target as Target model
    Draft-&amp;gt;&amp;gt;Draft: Propose K candidate tokens
    Draft-&amp;gt;&amp;gt;Target: Send draft prefix for verification
    Target-&amp;gt;&amp;gt;Target: Single forward pass over K positions
    alt Draft matches target distribution
        Target-&amp;gt;&amp;gt;Target: Accept longest matching prefix
    else Draft diverges at position i
        Target-&amp;gt;&amp;gt;Target: Accept tokens 1..i-1, resample from i
    end
    Target-&amp;gt;&amp;gt;Draft: Append accepted tokens, start next cycle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verifying K tokens costs roughly the same as generating one token autoregressively, so when the draft is correct you get K tokens for the price of one verification step.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Concrete Example
&lt;/h3&gt;

&lt;p&gt;Suppose the draft model proposes 5 tokens: &lt;code&gt;["I", " like", " cooking", " and", " traveling"]&lt;/code&gt;. The target model verifies them in a single forward pass:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Token&lt;/th&gt;
&lt;th&gt;Draft&lt;/th&gt;
&lt;th&gt;Target agrees?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;"I"&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;" like"&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;" cooking"&lt;/td&gt;
&lt;td&gt;✗ (target would say " playing")&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;" and"&lt;/td&gt;
&lt;td&gt;— (not evaluated)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;" traveling"&lt;/td&gt;
&lt;td&gt;— (not evaluated)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The target accepts tokens 1 and 2, then generates " playing" for token 3, producing three tokens in one cycle instead of three separate forward passes. If the draft had been correct through token 5, you'd get five tokens for the cost of one verification — a 5x speedup on that cycle alone.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Verification Bottleneck
&lt;/h3&gt;

&lt;p&gt;In practice, verification dominates execution time — 42-95% of the cycle, depending on the method and model size. The target model's forward pass is the bottleneck, and rejected tokens represent wasted compute.&lt;/p&gt;

&lt;p&gt;This is why acceptance rate matters so much. Every rejected token after the first is wasted verification work. The best speculative decoding methods maximize the expected accepted tokens per cycle, not just the raw acceptance rate.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Mathematical Guarantee
&lt;/h2&gt;

&lt;p&gt;One of the most important properties of speculative decoding is that it produces tokens from the &lt;strong&gt;exact same distribution&lt;/strong&gt; as standard autoregressive sampling from the target model. The verification step uses rejection sampling — when the draft proposes token x, the target model computes its own probability p(x) and the draft computes p_draft(x). The acceptance probability is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;min(1, p(x) / p_draft(x))&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When the target agrees (p(x) ≥ p_draft(x)), the token is always accepted. When the target disagrees, the token is accepted with probability proportional to the ratio, and rejected tokens are resampled from a residual distribution:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;r(x) = max(0, p(x) - p_draft(x)) / Σ max(0, p(y) - p_draft(y))&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This procedure guarantees that the output sequence follows the target model's distribution exactly, which is why speculative decoding is lossless. The draft model influences speed, not quality — the tokens you get are statistically indistinguishable from standard decoding, with the same perplexity and distribution. The only difference is latency.&lt;/p&gt;




&lt;h2&gt;
  
  
  Draft Model Strategies
&lt;/h2&gt;

&lt;p&gt;The draft mechanism is the variable that matters most. Different approaches have different tradeoffs between setup complexity, acceptance rate, and speedup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Standalone Draft Models
&lt;/h3&gt;

&lt;p&gt;The simplest approach loads a smaller model alongside the target — typically a 1B-3B model drafting for a 7B-70B target.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Conceptually straightforward&lt;/li&gt;
&lt;li&gt;Works with any target model&lt;/li&gt;
&lt;li&gt;Draft model can be tuned to match target's distribution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Requires loading a second model into VRAM (1-4 GB depending on size)&lt;/li&gt;
&lt;li&gt;Draft model quality directly determines acceptance rate&lt;/li&gt;
&lt;li&gt;Cross-family drafts (e.g., Qwen drafting for Llama) typically perform poorly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb:&lt;/strong&gt; Use models from the same family. Gemma 2 2B drafts well for Gemma 2 27B. Llama 3.2 1B drafts well for Llama 3.1 70B. Cross-family drafts tend to have low acceptance rates because the token distributions diverge.&lt;/p&gt;

&lt;h3&gt;
  
  
  Finding Compatible Draft Models
&lt;/h3&gt;

&lt;p&gt;Not all small models work as draft models for a given target. The critical factor is distribution alignment — how closely the draft model's output probabilities match the target's.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Target Model&lt;/th&gt;
&lt;th&gt;Recommended Draft&lt;/th&gt;
&lt;th&gt;Family Match&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Llama 3.1 70B&lt;/td&gt;
&lt;td&gt;Llama 3.2 1B-3B&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Llama 3.1 8B&lt;/td&gt;
&lt;td&gt;Llama 3.2 1B&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen 3 27B&lt;/td&gt;
&lt;td&gt;Qwen 3 0.6B-1.8B&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 2 27B&lt;/td&gt;
&lt;td&gt;Gemma 2 2B&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mixtral 8x7B&lt;/td&gt;
&lt;td&gt;Phi-3 4B (trained on Mixtral data)&lt;/td&gt;
&lt;td&gt;Cross (careful)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The golden rule: if the draft model's acceptance rate drops below 50%, speculative decoding may actually slow you down. The overhead of running the draft model plus verification outweighs the benefit when most proposals are rejected.&lt;/p&gt;




&lt;h2&gt;
  
  
  EAGLE and EAGLE-3: Prediction Heads
&lt;/h2&gt;

&lt;p&gt;EAGLE (Efficient Architecture Guided Language Model Estimation) eliminates the need for a separate draft model. Instead, it attaches lightweight autoregressive prediction heads to the target model's internal layers.&lt;/p&gt;

&lt;h3&gt;
  
  
  How EAGLE Works
&lt;/h3&gt;

&lt;p&gt;EAGLE trains prediction heads that take hidden states from the target model's intermediate layers and predict future tokens. During inference:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The target model runs a forward pass through its layers.&lt;/li&gt;
&lt;li&gt;At each layer, the EAGLE head reads the hidden state and proposes tokens for future positions.&lt;/li&gt;
&lt;li&gt;Multiple heads operate in parallel, each predicting a different future timestep.&lt;/li&gt;
&lt;li&gt;The target model verifies all proposals in a single pass.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The advantage: EAGLE heads are trained specifically to match the target model's distribution. They see the target's internal representations directly, which gives them much better alignment than a standalone draft model.&lt;/p&gt;

&lt;h3&gt;
  
  
  EAGLE-3 Improvements
&lt;/h3&gt;

&lt;p&gt;EAGLE-3 (2025) refines the approach with three key changes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Layer selection:&lt;/strong&gt; Instead of attaching heads to every layer, EAGLE-3 uses Bayesian optimization to select the optimal exit layer, reducing overhead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-token prediction:&lt;/strong&gt; Each head predicts multiple tokens simultaneously, increasing the draft depth without proportional compute cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Training efficiency:&lt;/strong&gt; EAGLE-3 trains on the target model's own generation data, improving acceptance rates on in-distribution workloads.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Acceptance rates:&lt;/strong&gt; EAGLE-3 typically achieves 60-80% acceptance rates on in-distribution workloads, compared to 40-60% for standalone draft models. On code generation workloads with high repetition, acceptance can exceed 85%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Setup:&lt;/strong&gt; EAGLE-3 requires pre-trained heads for your target model. NVIDIA provides EAGLE-3 heads for several popular models through TensorRT-LLM and the Speculative Decoding Modules collection on HuggingFace. Third-party implementations exist for vLLM and SGLang.&lt;/p&gt;

&lt;h3&gt;
  
  
  P-EAGLE: Parallel Drafting (March 2026)
&lt;/h3&gt;

&lt;p&gt;EAGLE-3's main limitation is autoregressive drafting — each draft token depends on the previous one, so generating K draft tokens requires K sequential forward passes through the draft head, and draft overhead grows linearly with K. P-EAGLE removes this ceiling by generating all K draft tokens in a single forward pass through a lightweight 4-layer drafter trained to predict up to 10 tokens in parallel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The result:&lt;/strong&gt; P-EAGLE delivers up to 1.69x speedup over vanilla EAGLE-3 on real workloads on NVIDIA B200. The advantage widens at higher K values — where EAGLE-3's sequential drafting becomes a bottleneck, P-EAGLE's parallel drafting incurs no additional cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Setup in vLLM:&lt;/strong&gt; Download a pre-trained P-EAGLE head from HuggingFace, set &lt;code&gt;"parallel_drafting": true&lt;/code&gt; in your vLLM config, and use the same &lt;code&gt;--speculative-model&lt;/code&gt; flag — vLLM handles the rest. P-EAGLE is the current state-of-the-art for EAGLE-based speculative decoding, and if you're deploying EAGLE in 2026, P-EAGLE is the variant to use.&lt;/p&gt;




&lt;h2&gt;
  
  
  n-gram Speculative Decoding
&lt;/h2&gt;

&lt;p&gt;n-gram speculative decoding replaces a neural draft with pattern matching against prompt history. The algorithm looks for repeated n-gram sequences in the context, and when the current token sequence matches a previously seen pattern, it proposes the tokens that followed that pattern earlier — for example, if the model has already generated &lt;code&gt;def calculate_total(items):&lt;/code&gt; and encounters &lt;code&gt;def calculate_total(&lt;/code&gt; again, it knows the next tokens will likely be &lt;code&gt;items):&lt;/code&gt; based on the previous occurrence.&lt;/p&gt;

&lt;p&gt;The n-gram map variants (&lt;code&gt;ngram-map-k&lt;/code&gt;, &lt;code&gt;ngram-map-k4v&lt;/code&gt;) use hash tables for faster lookups instead of linear scanning, with the hash key as the current n-gram of size N and the value as the token sequence that followed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zero VRAM overhead — no additional model to load (~16 MB for the hash table)&lt;/li&gt;
&lt;li&gt;Extremely fast for repetitive workloads (code editing, refactoring, template generation)&lt;/li&gt;
&lt;li&gt;Acceptance rates can reach 90%+ on workloads with high self-similarity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Useless for novel generation — if the pattern hasn't appeared before, n-gram has nothing to propose&lt;/li&gt;
&lt;li&gt;Acceptance rate drops to near zero on creative or diverse workloads&lt;/li&gt;
&lt;li&gt;Limited draft depth (typically 2-4 tokens per match)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Code refactoring, template filling, repetitive documentation, and any workload where the model revisits similar patterns. Worst for: creative writing, open-ended chat, and reasoning tasks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Parameter Tuning
&lt;/h3&gt;

&lt;p&gt;The n-gram parameters matter more than you'd expect. The defaults work for code, but text workloads need adjustment:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;Text&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;size-n&lt;/code&gt; (lookup length)&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;12-16&lt;/td&gt;
&lt;td&gt;8-10&lt;/td&gt;
&lt;td&gt;Longer n-grams reduce false positives but miss shorter patterns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;size-m&lt;/code&gt; (draft length)&lt;/td&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;Longer drafts mean more tokens per match, but also more rejections&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;min-hits&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Higher min-hits reduces false positives at the cost of fewer matches&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For text workloads, reduce &lt;code&gt;size-n&lt;/code&gt; to 8-10 and increase &lt;code&gt;min-hits&lt;/code&gt; to 2. This trades off match frequency for higher acceptance rates per match.&lt;/p&gt;




&lt;h2&gt;
  
  
  Self-Speculative Decoding
&lt;/h2&gt;

&lt;p&gt;Self-speculative decoding (also called LayerSkip or self-speculation) uses the model's own partial computation as the draft, so no separate model is needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  How It Works
&lt;/h3&gt;

&lt;p&gt;Instead of running the full model for each token, self-speculative decoding runs a truncated version — skipping some transformer layers — to generate draft tokens cheaply, and the full model then verifies the proposals.&lt;/p&gt;

&lt;p&gt;For example, a 32-layer model might run with only 16 layers for drafting, then verify with all 32 layers. The truncated forward pass is faster because it processes fewer layers, and the draft tokens benefit from seeing the same initial layers as the target.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No additional model weights to load&lt;/li&gt;
&lt;li&gt;Naturally aligned with the target distribution (same architecture, partial layers)&lt;/li&gt;
&lt;li&gt;Works well for models with significant redundancy in deeper layers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Requires modifying the inference engine to support partial forward passes&lt;/li&gt;
&lt;li&gt;KV cache complications — the draft uses partial KV cache that must be reconciled with the full model's cache&lt;/li&gt;
&lt;li&gt;Acceptance rates are typically lower than EAGLE or well-tuned draft models&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;llama.cpp implementation:&lt;/strong&gt; PR #18471 introduced self-speculative decoding using context history as draft. The model reuses tokens from its own generation history to propose continuations, particularly effective for coding workloads where patterns repeat within the same context window.&lt;/p&gt;




&lt;h2&gt;
  
  
  MTP (Multi-Token Prediction)
&lt;/h2&gt;

&lt;p&gt;MTP is a specialized form of speculative decoding built directly into certain model checkpoints. Qwen 3.6 ships both standard and MTP-enabled GGUF variants.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it differs:&lt;/strong&gt; MTP heads are baked into the model architecture during training. The model carries extra prediction heads that propose multiple future tokens in a single forward pass. There's no separate draft model — the MTP heads are part of the target model itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tradeoffs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No draft model to manage — MTP is activated with &lt;code&gt;--spec-type draft-mtp --spec-draft-n-max N&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;MTP heads add ~1-2 GB VRAM overhead&lt;/li&gt;
&lt;li&gt;Works best on MoE architectures (Qwen 3.6 35B-A3B) where sparse routing keeps MTP heads cheap&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For detailed benchmarks on MTP vs standard decoding across Qwen 3.6 27B and 35B, see &lt;a href="https://www.glukhov.org/llm-performance/benchmarks/comparing-qwen-3-6-mtp-vs-standard/" rel="noopener noreferrer"&gt;Qwen 3.6 MTP vs Standard on 16GB GPU&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Acceptance Rates: What They Mean in Practice
&lt;/h2&gt;

&lt;p&gt;Acceptance rate (α) is the single most important metric for speculative decoding performance. It determines whether you're getting a speedup or paying overhead.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Speedup Formula
&lt;/h3&gt;

&lt;p&gt;Expected accepted tokens per verification pass:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;E[accepted] = α × K&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Where K is the number of draft tokens proposed per cycle. If α = 0.7 and K = 5, you accept 3.5 tokens per pass — a 3.5x speedup over standard decoding (which produces 1 token per pass).&lt;/p&gt;

&lt;h3&gt;
  
  
  Acceptance Rate by Method
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Typical α Range&lt;/th&gt;
&lt;th&gt;Best Workload&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Draft model (same family)&lt;/td&gt;
&lt;td&gt;40-60%&lt;/td&gt;
&lt;td&gt;General chat, reasoning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Draft model (cross-family)&lt;/td&gt;
&lt;td&gt;20-40%&lt;/td&gt;
&lt;td&gt;Rarely recommended&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EAGLE-3&lt;/td&gt;
&lt;td&gt;60-80%&lt;/td&gt;
&lt;td&gt;General workloads, code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P-EAGLE&lt;/td&gt;
&lt;td&gt;65-85%&lt;/td&gt;
&lt;td&gt;General workloads, deeper speculation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;n-gram&lt;/td&gt;
&lt;td&gt;10-90%+&lt;/td&gt;
&lt;td&gt;Workload-dependent (high on repetitive, near zero on novel)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MTP&lt;/td&gt;
&lt;td&gt;50-70%&lt;/td&gt;
&lt;td&gt;Qwen 3.6 models specifically&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-speculative&lt;/td&gt;
&lt;td&gt;30-50%&lt;/td&gt;
&lt;td&gt;Coding, repetitive patterns&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  When Acceptance Rate Drops
&lt;/h3&gt;

&lt;p&gt;Acceptance rate is not constant across a generation. It varies by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Token position:&lt;/strong&gt; Early tokens tend to have higher acceptance (more context, less uncertainty). Later tokens drop as the model explores more diverse continuations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workload type:&lt;/strong&gt; Code editing with repeated patterns sees α &amp;gt; 80%. Open-ended creative writing sees α &amp;lt; 40%.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Temperature:&lt;/strong&gt; Higher temperature increases divergence between draft and target, lowering acceptance. Speculative decoding works best at low temperature (0.0-0.7).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Critical threshold:&lt;/strong&gt; If your effective acceptance rate (α × K) drops below 1.0, speculative decoding is slower than standard decoding. The draft overhead plus verification time exceeds the cost of a single autoregressive step.&lt;/p&gt;




&lt;h2&gt;
  
  
  Speculative Decoding in Production: What Actually Happens
&lt;/h2&gt;

&lt;p&gt;Research papers report 2-4x speedups, but production benchmarks tell a more nuanced story — speedups shrink with batch size, verification dominates cycle time, and no single method wins on every workload.&lt;/p&gt;

&lt;h3&gt;
  
  
  SpecDecode-Bench Findings (2026)
&lt;/h3&gt;

&lt;p&gt;A systematic evaluation of five SD variants (n-gram, EAGLE, EAGLE-3, Draft-Model, MTP) on vLLM across four models and six workloads revealed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SD works, but speedups shrink with batch size.&lt;/strong&gt; At batch size 1, EAGLE achieves up to 1.96x on Llama-3-70B. By batch size 128, this drops to 1.21x. The system becomes compute-bound at high concurrency, and the GPU has less idle capacity to spare for speculation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Verification dominates execution time (42-95%).&lt;/strong&gt; The target model's forward pass is the bottleneck. Reducing wasted verification on rejected tokens is the most promising avenue for improvement.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No single method wins everywhere.&lt;/strong&gt; EAGLE-3 is the best all-around choice. Draft-model methods excel when the target model is large (70B+). n-gram is optimal for code editing and high-overlap tasks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Oracle analysis reveals a gap.&lt;/strong&gt; The theoretical upper bound for combined n-gram + EAGLE strategies reaches ~4.9x on code editing workloads, but current implementations achieve 2-3x. There's room for optimization.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Practical Speedup Expectations
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Expected Speedup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;70B model, single request, EAGLE-3&lt;/td&gt;
&lt;td&gt;1.5-2.0x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;70B model, batch 32, EAGLE-3&lt;/td&gt;
&lt;td&gt;1.2-1.5x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8B model, single request, draft model&lt;/td&gt;
&lt;td&gt;1.3-1.8x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code editing, n-gram&lt;/td&gt;
&lt;td&gt;2.0-4.0x (workload dependent)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Creative writing, any method&lt;/td&gt;
&lt;td&gt;1.0-1.3x (often not worth it)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MTP on Qwen 3.6 27B, 16GB GPU&lt;/td&gt;
&lt;td&gt;1.5-1.7x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P-EAGLE on B200, single request&lt;/td&gt;
&lt;td&gt;2.0-3.0x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The batch size effect is critical. At small batches, the GPU has idle compute to spare for speculation. At large batches, the system is already saturated, and speculative decoding adds overhead without proportional benefit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Monitoring in Production
&lt;/h3&gt;

&lt;p&gt;You should track acceptance rate in production. A declining acceptance rate signals that your draft model is diverging from the target — either because the workload changed, or because the draft model needs retraining.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key metrics to monitor:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Acceptance rate per request (should be stable around your baseline)&lt;/li&gt;
&lt;li&gt;Tokens per second with vs without speculative decoding (the actual speedup)&lt;/li&gt;
&lt;li&gt;Verification time as percentage of cycle time (should be 42-95%)&lt;/li&gt;
&lt;li&gt;Draft model forward pass time (should be &amp;lt; 20% of target model time)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your acceptance rate drops below 40%, disable speculative decoding for that request. The overhead is not worth it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Practical Setup
&lt;/h2&gt;

&lt;p&gt;Engine choice matters as much as draft strategy — see &lt;a href="https://www.glukhov.org/llm-hosting/comparisons/hosting-llms-ollama-localai-jan-lmstudio-vllm-comparison/" rel="noopener noreferrer"&gt;Ollama vs vLLM vs LM Studio and other local runtimes&lt;/a&gt; for how each runtime handles batching, API compatibility, and throughput before you pick a speculative decoding path.&lt;/p&gt;

&lt;h3&gt;
  
  
  llama.cpp
&lt;/h3&gt;

&lt;p&gt;For general server setup and GGUF loading, start with the &lt;a href="https://www.glukhov.org/llm-hosting/llama-cpp/" rel="noopener noreferrer"&gt;llama.cpp quickstart&lt;/a&gt;; the flags below add speculative decoding on top.&lt;/p&gt;

&lt;p&gt;llama.cpp supports multiple speculative decoding methods through the &lt;code&gt;--spec-type&lt;/code&gt; flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Draft model (standalone)&lt;/span&gt;
llama-server &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--model&lt;/span&gt; target-model.gguf &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--draft-model&lt;/span&gt; draft-model.gguf &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--spec-draft-n-max&lt;/span&gt; 4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--parallel&lt;/span&gt; 1  &lt;span class="c"&gt;# Mandatory: --parallel 1 for speculative decoding&lt;/span&gt;

&lt;span class="c"&gt;# n-gram&lt;/span&gt;
llama-server &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--model&lt;/span&gt; target-model.gguf &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--spec-type&lt;/span&gt; ngram-simple &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--spec-ngram-simple-size-n&lt;/span&gt; 12 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--spec-ngram-simple-size-m&lt;/span&gt; 48

&lt;span class="c"&gt;# n-gram (text workload tuning)&lt;/span&gt;
llama-server &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--model&lt;/span&gt; target-model.gguf &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--spec-type&lt;/span&gt; ngram-simple &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--spec-ngram-simple-size-n&lt;/span&gt; 8 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--spec-ngram-simple-size-m&lt;/span&gt; 32 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--spec-ngram-simple-min-hits&lt;/span&gt; 2

&lt;span class="c"&gt;# MTP (Qwen 3.6)&lt;/span&gt;
llama-server &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--model&lt;/span&gt; Qwen3.6-27B-MTP.gguf &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--spec-type&lt;/span&gt; draft-mtp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--spec-draft-n-max&lt;/span&gt; 2

&lt;span class="c"&gt;# Self-speculative (coding workloads)&lt;/span&gt;
llama-server &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--model&lt;/span&gt; target-model.gguf &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--spec-type&lt;/span&gt; draft-self
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Critical flags:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--parallel 1&lt;/code&gt; — Speculative decoding in llama.cpp requires single-batch mode. This is a current limitation.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--spec-draft-n-max&lt;/code&gt; — Number of draft tokens per cycle. Start with 3-5; higher values increase VRAM pressure.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--spec-ngram-simple-size-n&lt;/code&gt; — Lookup n-gram length. Default 12 works well for code; reduce to 8 for text.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common pitfalls:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Forgetting &lt;code&gt;--parallel 1&lt;/code&gt; — the server will silently ignore speculative decoding.&lt;/li&gt;
&lt;li&gt;Using cross-family draft models — acceptance rates collapse, negating any speedup.&lt;/li&gt;
&lt;li&gt;Setting &lt;code&gt;--spec-draft-n-max&lt;/code&gt; too high — each extra draft token costs VRAM for the draft buffer. Diminishing returns kick in around 5-8.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  vLLM
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://www.glukhov.org/llm-hosting/vllm/vllm-quickstart/" rel="noopener noreferrer"&gt;vLLM quickstart&lt;/a&gt; covers base deployment; the flags below enable speculative decoding on an existing vLLM server.&lt;/p&gt;

&lt;p&gt;vLLM supports speculative decoding through the &lt;code&gt;--speculative-model&lt;/code&gt; and &lt;code&gt;--speculative-num-steps&lt;/code&gt; flags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Draft model&lt;/span&gt;
vllm serve target-model &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--speculative-model&lt;/span&gt; draft-model &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--speculative-num-steps&lt;/span&gt; 5 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--speculative-accept-length&lt;/span&gt; 5

&lt;span class="c"&gt;# EAGLE-3&lt;/span&gt;
vllm serve target-model &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--speculative-model&lt;/span&gt; EAGLE-target-model/ &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--speculative-num-steps&lt;/span&gt; 7 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--speculative-draft-tensor-parallel-size&lt;/span&gt; 1

&lt;span class="c"&gt;# P-EAGLE (parallel drafting)&lt;/span&gt;
vllm serve target-model &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--speculative-model&lt;/span&gt; P-EAGLE-target-model/ &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--speculative-num-steps&lt;/span&gt; 7 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--speculative-parallel-drafting&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;

&lt;span class="c"&gt;# n-gram&lt;/span&gt;
vllm serve target-model &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--speculative-method&lt;/span&gt; ngram &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--speculative-num-steps&lt;/span&gt; 5 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ngram-context-size&lt;/span&gt; 12
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;vLLM's speculative decoding is integrated with continuous batching, so it works under concurrent workloads. The scheduler handles multiple token slots within a single forward pass, and the memory manager handles KV cache for both draft and target models.&lt;/p&gt;

&lt;h3&gt;
  
  
  SGLang
&lt;/h3&gt;

&lt;p&gt;SGLang supports speculative decoding through its &lt;code&gt;--speculative-algorithm&lt;/code&gt; flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; sglang.launch_server &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--model-path&lt;/span&gt; target-model &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--speculative-algorithm&lt;/span&gt; ngram &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ngram-context-size&lt;/span&gt; 12 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ngram-max-candidate-tokens&lt;/span&gt; 6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SGLang's RadixAttention architecture pairs well with speculative decoding because prefix caching reduces verification cost — the target model reuses cached attention for shared prefixes, making each verification pass cheaper than a cold forward pass.&lt;/p&gt;

&lt;h3&gt;
  
  
  TensorRT-LLM
&lt;/h3&gt;

&lt;p&gt;TensorRT-LLM provides production-grade speculative decoding with Triton Inference Server. The setup is more involved but offers the best performance on NVIDIA hardware:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build the TensorRT engine for both target and draft models.&lt;/li&gt;
&lt;li&gt;Configure the model repository with &lt;code&gt;model.yaml&lt;/code&gt; specifying the speculative decoding configuration.&lt;/li&gt;
&lt;li&gt;Launch Triton with the LLM API / PyTorch backend.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;TensorRT-LLM supports both draft-model and EAGLE-3 variants. For code generation workloads, TensorRT-LLM with n-gram speculative decoding has demonstrated 2-3x latency reduction in production deployments.&lt;/p&gt;




&lt;h2&gt;
  
  
  When to Use Speculative Decoding
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Use It When
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Large target models (7B+):&lt;/strong&gt; The overhead of the draft mechanism is amortized across the target's compute. Speculative decoding shines when the target model is slow — the larger the target, the more valuable the speedup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low-temperature workloads:&lt;/strong&gt; Speculative decoding works best at temperature 0.0-0.7, where the target model's distribution is concentrated and the draft has a better chance of matching.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interactive applications:&lt;/strong&gt; Latency-sensitive workloads (chat, code completion, agent tool calls) benefit most. Batch processing where you're already saturating the GPU sees less benefit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code generation and editing:&lt;/strong&gt; High repetition in code patterns makes n-gram and self-speculative decoding particularly effective.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Skip It When
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Small target models (&amp;lt; 3B):&lt;/strong&gt; The draft model's overhead approaches the target's forward pass time. The speedup is marginal or negative.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High-temperature sampling:&lt;/strong&gt; At temperature &amp;gt; 0.7, the target model's distribution is too broad for the draft to match reliably.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Creative writing and open-ended generation:&lt;/strong&gt; Low acceptance rates on novel content make the overhead not worth it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High batch sizes (&amp;gt; 32):&lt;/strong&gt; The system becomes compute-bound, and speculative decoding adds overhead without proportional benefit. SpecDecode-Bench shows speedup dropping from 1.96x to 1.21x as batch size goes from 1 to 128.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Combining Methods
&lt;/h2&gt;

&lt;p&gt;Advanced setups combine multiple speculative decoding strategies. The SpecDecode-Bench oracle analysis showed that adaptively combining n-gram and EAGLE can push speedup to 4.9x on code editing workloads.&lt;/p&gt;

&lt;p&gt;The idea is to use n-gram for patterns that have appeared before, where acceptance is high and overhead is near zero, and fall back to EAGLE for novel tokens. In practice this requires engine support for multi-method speculation — vLLM and TensorRT-LLM have experimental support, but production-grade implementations are still maturing.&lt;/p&gt;

&lt;p&gt;For now, the most practical combination is MTP + n-gram in llama.cpp. MTP handles the neural speculation, while n-gram catches repetitive patterns that MTP misses. On Qwen 3 27B, this combination achieves 120 tokens/sec compared to 67 tokens/sec standard — a 1.8x speedup.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cost Considerations
&lt;/h2&gt;

&lt;p&gt;Speculative decoding trades compute for latency. The total compute per token is roughly the same — you're just doing more work in parallel rather than sequentially.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GPU cost impact:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Single-request latency improves by 20-50%, which matters for interactive applications.&lt;/li&gt;
&lt;li&gt;Throughput (tokens/sec across many requests) improves less — the GPU is already saturated at high batch sizes.&lt;/li&gt;
&lt;li&gt;VRAM usage increases by the draft model's footprint (1-4 GB for standalone drafts, minimal for n-gram/EAGLE).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cloud inference:&lt;/strong&gt; At $2-4/hr per H100, speculative decoding reduces per-request latency without increasing per-token cost. For batch processing where you're already saturating the GPU, the cost benefit is minimal — you're paying for the same GPU time either way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When speculative decoding saves money:&lt;/strong&gt; Interactive applications where you charge per request and want to reduce time-to-first-token. A 2x speedup means your users wait half as long, and you can serve more requests per second on the same hardware.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When it doesn't:&lt;/strong&gt; Batch processing where you're already maximizing GPU utilization. The extra compute from speculative decoding doesn't increase throughput — it just changes the latency profile.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Speculative decoding is maturing from research novelty to production standard. The frontier is pushing beyond the current limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Speculative Speculative Decoding (SSD):&lt;/strong&gt; Parallelizes the drafting and verification stages across separate hardware. The draft model runs asynchronously, pre-speculating for multiple likely verification outcomes. Early results show up to 2x speedup over optimized speculative decoding, and 5x over autoregressive decoding. Not production-ready yet, but the direction is clear.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SpecSA (Sparse Speculative Verification):&lt;/strong&gt; Combines speculative decoding with dynamic sparse attention. Turns sparse attention into a verification-oriented workload, achieving up to 3.49x end-to-end throughput over autoregressive sparse decoding. Relevant for long-context models where sparse attention is already in use.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Adaptive speculation:&lt;/strong&gt; Automatically switching between n-gram, EAGLE, and draft-model methods based on workload characteristics. The oracle analysis shows significant untapped potential — current implementations achieve 2-3x, but the theoretical bound is 4.9x.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Multimodal speculative decoding:&lt;/strong&gt; Extending draft-verify to vision-language models and video generation. Early surveys show the same principles apply, but verification strategies need adaptation for non-text modalities.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Decision Framework
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Answer&lt;/th&gt;
&lt;th&gt;Recommendation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Target model size?&lt;/td&gt;
&lt;td&gt;&amp;lt; 3B&lt;/td&gt;
&lt;td&gt;Skip speculative decoding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Target model size?&lt;/td&gt;
&lt;td&gt;7-13B&lt;/td&gt;
&lt;td&gt;Use n-gram or self-speculative (low overhead)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Target model size?&lt;/td&gt;
&lt;td&gt;30B+&lt;/td&gt;
&lt;td&gt;Use draft model or EAGLE-3 (larger target = more benefit)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workload type?&lt;/td&gt;
&lt;td&gt;Code editing/refactoring&lt;/td&gt;
&lt;td&gt;n-gram + EAGLE combination&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workload type?&lt;/td&gt;
&lt;td&gt;General chat&lt;/td&gt;
&lt;td&gt;EAGLE-3 or P-EAGLE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workload type?&lt;/td&gt;
&lt;td&gt;Creative writing&lt;/td&gt;
&lt;td&gt;Skip speculative decoding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch size?&lt;/td&gt;
&lt;td&gt;1-4 (interactive)&lt;/td&gt;
&lt;td&gt;Speculative decoding helps most&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch size?&lt;/td&gt;
&lt;td&gt;32+ (throughput)&lt;/td&gt;
&lt;td&gt;Speculative decoding helps less&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Temperature?&lt;/td&gt;
&lt;td&gt;0.0-0.7&lt;/td&gt;
&lt;td&gt;Good for speculative decoding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Temperature?&lt;/td&gt;
&lt;td&gt;&amp;gt; 0.7&lt;/td&gt;
&lt;td&gt;Skip speculative decoding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hardware?&lt;/td&gt;
&lt;td&gt;16GB GPU&lt;/td&gt;
&lt;td&gt;Use n-gram or MTP (low VRAM overhead)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hardware?&lt;/td&gt;
&lt;td&gt;24GB+ GPU&lt;/td&gt;
&lt;td&gt;Draft model or EAGLE-3 feasible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Engine?&lt;/td&gt;
&lt;td&gt;vLLM&lt;/td&gt;
&lt;td&gt;EAGLE-3 or P-EAGLE (best integration)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Engine?&lt;/td&gt;
&lt;td&gt;llama.cpp&lt;/td&gt;
&lt;td&gt;n-gram or MTP (simplest setup)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Engine?&lt;/td&gt;
&lt;td&gt;TensorRT-LLM&lt;/td&gt;
&lt;td&gt;EAGLE-3 or draft model (production-grade)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>aicoding</category>
    </item>
    <item>
      <title>Transactional Outbox Pattern in Go with PostgreSQL</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Sun, 05 Jul 2026 08:48:20 +0000</pubDate>
      <link>https://dev.to/rosgluk/transactional-outbox-pattern-in-go-with-postgresql-59an</link>
      <guid>https://dev.to/rosgluk/transactional-outbox-pattern-in-go-with-postgresql-59an</guid>
      <description>&lt;p&gt;Two writes that should succeed together will eventually fail separately.&lt;br&gt;
Your order service saves the order to the database, then publishes an &lt;code&gt;order.created&lt;/code&gt; event to a message broker. &lt;/p&gt;



&lt;p&gt;These two operations run one after the other.&lt;/p&gt;

&lt;p&gt;Between them, things go wrong: the broker is down, the network times out, the process restarts, or the container is evicted. The database write succeeded. The publish did not. The downstream service that needs to know about the new order never finds out. Nobody noticed until a customer called.&lt;/p&gt;

&lt;p&gt;This is the dual-write problem, and it is one of the most common sources of silent data loss in distributed systems. The transactional outbox pattern is the standard fix.&lt;/p&gt;
&lt;h2&gt;
  
  
  The dual-write problem
&lt;/h2&gt;

&lt;p&gt;The failure mode is easy to reason about once you see it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BEGIN;
  INSERT INTO orders ...   -- succeeds
COMMIT;

PUBLISH order.created ...  -- fails, crashes, or is never reached
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The database and the message broker do not share a transaction boundary. There is no rollback that covers both. Every service that does &lt;code&gt;save -&amp;gt; publish&lt;/code&gt; in sequence has this gap. The pattern shows up in many forms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;db.Save(order)&lt;/code&gt; followed by &lt;code&gt;events.Publish(OrderCreated{...})&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;HTTP handler that commits a transaction and then calls an external webhook&lt;/li&gt;
&lt;li&gt;Worker that processes a record from one queue and writes results to another&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The outcome in all cases is the same: one side succeeds while the other fails, and the system ends up in a state that is invisible to monitoring because both individual operations returned success at some point.&lt;/p&gt;

&lt;p&gt;A retry loop does not fix this. Retrying the publish after the database commit only works if the retry itself is reliable -- which requires exactly the durability guarantee you do not have.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the transactional outbox pattern does
&lt;/h2&gt;

&lt;p&gt;The outbox pattern eliminates the gap by removing the direct publish entirely. Instead of calling the broker from within your business logic, you write an event record into an &lt;code&gt;outbox&lt;/code&gt; table in the same database transaction as the business data. A separate background process -- the relay -- reads from the outbox table and publishes to the broker.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BEGIN;
  INSERT INTO orders ...         -- business data
  INSERT INTO outbox_events ...  -- event record
COMMIT;

-- Relay process (separately):
SELECT ... FROM outbox_events FOR UPDATE SKIP LOCKED;
PUBLISH order.created ...
UPDATE outbox_events SET processed_at = NOW() WHERE id = $1;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both writes succeed or both fail. The transaction guarantee you already have from PostgreSQL now covers the event record too. The relay can retry publishing as many times as needed because the event sits in durable storage. If the relay crashes mid-flight, it restarts and retries. The worst outcome is that the event is published more than once -- which is handled by making consumers idempotent (see &lt;a href="https://www.glukhov.org/app-architecture/integration-patterns/idempotency-in-distributed-systems/" rel="noopener noreferrer"&gt;Idempotency in Distributed Systems&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  PostgreSQL schema for the outbox table
&lt;/h2&gt;

&lt;p&gt;The schema is intentionally simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;outbox_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;             &lt;span class="n"&gt;UUID&lt;/span&gt;         &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;aggregate_type&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;aggregate_id&lt;/span&gt;   &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_type&lt;/span&gt;     &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt;        &lt;span class="n"&gt;JSONB&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;attempts&lt;/span&gt;       &lt;span class="nb"&gt;INT&lt;/span&gt;          &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt;     &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;  &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;processed_at&lt;/span&gt;   &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Partial index: only indexes unprocessed rows, stays small as rows are marked done&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_outbox_unprocessed&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;outbox_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;processed_at&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The partial index on &lt;code&gt;created_at WHERE processed_at IS NULL&lt;/code&gt; is important. Without it, the index grows with every event ever written and the relay's polling query gets slower over time. With it, the index covers only the pending rows, which in steady state is a small, bounded set regardless of how many events have been published.&lt;/p&gt;

&lt;p&gt;Key field choices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;aggregate_type&lt;/code&gt; and &lt;code&gt;aggregate_id&lt;/code&gt; describe which entity the event belongs to. Useful for ordering guarantees and routing.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;event_type&lt;/code&gt; is the event name your consumers expect.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;payload JSONB&lt;/code&gt; stores the event body. Use &lt;code&gt;JSONB&lt;/code&gt; rather than &lt;code&gt;TEXT&lt;/code&gt; so you can query it if needed.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;attempts&lt;/code&gt; tracks how many times the relay has tried to publish this row. Used for retry limits and dead-letter handling.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;processed_at&lt;/code&gt; is &lt;code&gt;NULL&lt;/code&gt; for pending rows and set when the relay successfully publishes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Writing business data and outbox event in one transaction
&lt;/h2&gt;

&lt;p&gt;The business logic writes both records inside a single &lt;code&gt;BeginTx&lt;/code&gt; / &lt;code&gt;Commit&lt;/code&gt; call. There is no publish call here -- only database writes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;OrderService&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;CreateOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BeginTx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"begin tx: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Rollback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExecContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;`
        INSERT INTO orders (id, customer_id, total, created_at)
        VALUES ($1, $2, $3, NOW())
    `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CustomerID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"insert order: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Marshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"order_id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"customer_id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CustomerID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"total"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"marshal payload: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExecContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;`
        INSERT INTO outbox_events
            (aggregate_type, aggregate_id, event_type, payload)
        VALUES ($1, $2, $3, $4)
    `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"order"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"order.created"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"insert outbox event: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;tx.Commit()&lt;/code&gt; fails, neither the order row nor the outbox row is persisted. If it succeeds, both are guaranteed to be in the database. The relay can publish the event at any point after that -- immediately, in one second, or after the relay restarts following a crash.&lt;/p&gt;

&lt;p&gt;This is the only code change required in your business layer. The rest of the pattern lives in the relay.&lt;/p&gt;

&lt;h2&gt;
  
  
  Go relay implementation
&lt;/h2&gt;

&lt;p&gt;The relay is a background worker that polls the outbox table on a timer. It fetches a batch of unprocessed rows, publishes each one, and marks it done. Keep it in the same binary as your application or run it as a separate process -- either works, but the same binary is simpler to operate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;OutboxRelay&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;          &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;
    &lt;span class="n"&gt;publisher&lt;/span&gt;   &lt;span class="n"&gt;Publisher&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;      &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;slog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Logger&lt;/span&gt;
    &lt;span class="n"&gt;batchSize&lt;/span&gt;   &lt;span class="kt"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;pollInterval&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;
    &lt;span class="n"&gt;maxAttempts&lt;/span&gt;  &lt;span class="kt"&gt;int&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;OutboxRelay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ticker&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewTicker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pollInterval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;ticker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;ticker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;processBatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"outbox relay batch failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"err"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The relay respects context cancellation, which makes it straightforward to integrate with graceful shutdown. For a detailed treatment of context lifetime and cancellation patterns, see &lt;a href="https://www.glukhov.org/app-architecture/code-architecture/go-context-cancellation-timeouts/" rel="noopener noreferrer"&gt;Go context.Context Done Right&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  FOR UPDATE SKIP LOCKED: the concurrent worker pattern
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;processBatch&lt;/code&gt; function uses &lt;code&gt;FOR UPDATE SKIP LOCKED&lt;/code&gt; to safely handle concurrent relay workers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;OutboxRelay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;processBatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BeginTx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"begin tx: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Rollback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QueryContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;`
        SELECT id, aggregate_type, aggregate_id, event_type, payload
        FROM outbox_events
        WHERE processed_at IS NULL
          AND attempts &amp;lt; $1
        ORDER BY created_at
        LIMIT $2
        FOR UPDATE SKIP LOCKED
    `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;maxAttempts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;batchSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"query outbox: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;id&lt;/span&gt;            &lt;span class="kt"&gt;string&lt;/span&gt;
        &lt;span class="n"&gt;aggregateType&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
        &lt;span class="n"&gt;aggregateID&lt;/span&gt;   &lt;span class="kt"&gt;string&lt;/span&gt;
        &lt;span class="n"&gt;eventType&lt;/span&gt;     &lt;span class="kt"&gt;string&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt;       &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RawMessage&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;aggregateType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;aggregateID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;eventType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"scan row: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;publisher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;eventType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;aggregateID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"publish failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"event_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"err"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExecContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s"&gt;`UPDATE outbox_events SET attempts = attempts + 1 WHERE id = $1`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"increment attempts failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"event_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"err"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExecContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;`UPDATE outbox_events SET processed_at = NOW() WHERE id = $1`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"mark processed: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;FOR UPDATE SKIP LOCKED&lt;/code&gt; does two things. First, &lt;code&gt;FOR UPDATE&lt;/code&gt; locks the selected rows for the duration of the transaction, preventing any other transaction from selecting them. Second, &lt;code&gt;SKIP LOCKED&lt;/code&gt; means that if a row is already locked by another transaction, the query skips it instead of waiting. The result is that multiple relay workers can run in parallel and each will pick up a non-overlapping subset of rows.&lt;/p&gt;

&lt;p&gt;Without &lt;code&gt;SKIP LOCKED&lt;/code&gt;, a second worker would block until the first transaction commits before seeing the same rows -- at which point they would already be marked done. With &lt;code&gt;SKIP LOCKED&lt;/code&gt;, the second worker immediately picks up different rows instead of waiting, giving you safe horizontal scaling.&lt;/p&gt;

&lt;p&gt;Note the scan-then-publish separation in the code above: all rows are scanned into a slice before the publish loop starts. This avoids holding an open &lt;code&gt;*sql.Rows&lt;/code&gt; cursor across network calls to the broker, which would hold the transaction open longer than necessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idempotency and deduplication
&lt;/h2&gt;

&lt;p&gt;The relay publishes at least once. If it publishes an event and then crashes before committing the &lt;code&gt;processed_at&lt;/code&gt; update, it will publish the same event again on restart. This is unavoidable -- exactly-once delivery across a database and a message broker without a distributed transaction coordinator requires this trade-off.&lt;/p&gt;

&lt;p&gt;Consumers must be idempotent. The simplest approach is to track processed event IDs in a &lt;code&gt;processed_events&lt;/code&gt; table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;processed_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;event_id&lt;/span&gt;   &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;processed_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;OrderHandler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;HandleOrderCreated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;eventID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// Deduplicate using the event ID as the natural key&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExecContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;`
        INSERT INTO processed_events (event_id) VALUES ($1)
        ON CONFLICT (event_id) DO NOTHING
    `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;eventID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dedup check: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// Check if the insert actually happened (1 row) or was a no-op (0 rows)&lt;/span&gt;
    &lt;span class="c"&gt;// A simpler approach: use RETURNING or check rows affected&lt;/span&gt;
    &lt;span class="c"&gt;// If 0 rows affected, this is a duplicate -- skip it&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In practice, many teams rely on the broker's own deduplication headers (such as Kafka's &lt;code&gt;key&lt;/code&gt; field for log-compacted topics, or RabbitMQ's &lt;code&gt;message-id&lt;/code&gt; header) and treat database-level deduplication as a fallback. Both are valid layers to apply.&lt;/p&gt;

&lt;p&gt;Include the outbox event &lt;code&gt;id&lt;/code&gt; (a UUID) in the published message as the deduplication key. Consumers can then use it regardless of which deduplication mechanism they prefer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retry policy and poison messages
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;attempts&lt;/code&gt; column drives the retry policy. The relay skips rows where &lt;code&gt;attempts &amp;gt;= maxAttempts&lt;/code&gt; and treats those rows as dead letters. A separate process or operator alert handles them.&lt;/p&gt;

&lt;p&gt;A simple dead-letter view:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;outbox_dead_letters&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;outbox_events&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;processed_at&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A good production retry policy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set &lt;code&gt;maxAttempts&lt;/code&gt; to 5-10 depending on how expensive retries are.&lt;/li&gt;
&lt;li&gt;Consider exponential backoff: include a &lt;code&gt;retry_after&lt;/code&gt; column and skip rows where &lt;code&gt;retry_after &amp;gt; NOW()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Alert on &lt;code&gt;COUNT(*) FROM outbox_dead_letters&lt;/code&gt; exceeding a threshold.&lt;/li&gt;
&lt;li&gt;Provide a manual retry path: an admin endpoint or script that resets &lt;code&gt;attempts = 0&lt;/code&gt; and &lt;code&gt;retry_after = NULL&lt;/code&gt; for specific rows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Poison messages -- rows that consistently fail due to a bug in the consumer or a schema mismatch -- should not block healthy messages. Since the relay processes a batch per tick and marks failures with an attempt increment rather than removing them from the queue, healthy rows proceed normally while poisoned ones accumulate attempts until they hit the dead-letter threshold.&lt;/p&gt;

&lt;h2&gt;
  
  
  Event ordering and partitioning
&lt;/h2&gt;

&lt;p&gt;The polling query orders by &lt;code&gt;created_at&lt;/code&gt;, which gives first-in-first-out ordering within a batch. For most use cases that is enough. When strict per-entity ordering matters -- for example, ensuring that &lt;code&gt;order.updated&lt;/code&gt; is never published before &lt;code&gt;order.created&lt;/code&gt; for the same order -- you need per-aggregate ordering.&lt;/p&gt;

&lt;p&gt;Add &lt;code&gt;aggregate_id&lt;/code&gt; to the &lt;code&gt;ORDER BY&lt;/code&gt; clause and use it as the message key when publishing to a partitioned topic like &lt;a href="https://www.glukhov.org/data-infrastructure/stream-processing/apache-kafka/" rel="noopener noreferrer"&gt;Apache Kafka&lt;/a&gt;. Kafka routes all messages with the same key to the same partition, and partitions are consumed in order. This gives you per-aggregate ordering guarantees without global ordering, which would require a single relay instance.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;aggregate_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For brokers that do not support partitioned ordering (such as basic AMQP queues), single-instance relay or application-level ordering checks in the consumer are the practical alternatives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reduce polling latency with LISTEN/NOTIFY
&lt;/h2&gt;

&lt;p&gt;A polling interval of one second means average event latency of 500 milliseconds. For most workloads that is fine. For cases where you need near-zero latency, PostgreSQL's &lt;code&gt;LISTEN/NOTIFY&lt;/code&gt; mechanism lets the relay wake up immediately when a new outbox row is inserted.&lt;/p&gt;

&lt;p&gt;Add a trigger to the outbox table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;notify_outbox_insert&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="k"&gt;trigger&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="n"&gt;PERFORM&lt;/span&gt; &lt;span class="n"&gt;pg_notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'outbox_event'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt; &lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="n"&gt;outbox_insert_notify&lt;/span&gt;
&lt;span class="k"&gt;AFTER&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;outbox_events&lt;/span&gt;
&lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;EACH&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;notify_outbox_insert&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the relay, listen on the channel and wake up on notifications while still falling back to periodic polling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;OutboxRelay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;listener&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;pq&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dsn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Minute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;listener&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;listener&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"outbox_event"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"listen: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;ticker&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewTicker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// fallback poll&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;ticker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;listener&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Notify&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;processBatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"outbox batch failed (notify)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"err"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;ticker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;processBatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"outbox batch failed (poll)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"err"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fallback ticker handles any notifications missed during a relay restart or network hiccup. Keep the fallback interval at a few seconds rather than milliseconds -- its job is recovery, not low latency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observability: metrics, logs, and alerts
&lt;/h2&gt;

&lt;p&gt;The outbox is infrastructure. Treat it like infrastructure and instrument it accordingly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key metrics:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;outboxPublished&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prometheus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewCounter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prometheus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CounterOpts&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"outbox_events_published_total"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Help&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Total outbox events successfully published."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;outboxFailed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prometheus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewCounterVec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prometheus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CounterOpts&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"outbox_events_failed_total"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Help&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Total outbox publish failures by event type."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"event_type"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;outboxPending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prometheus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewGauge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prometheus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GaugeOpts&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"outbox_events_pending"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Help&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Current number of unprocessed outbox events."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;outboxBatchDuration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prometheus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewHistogram&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prometheus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HistogramOpts&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;"outbox_batch_duration_seconds"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Help&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;"Duration of each outbox processing batch."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Buckets&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prometheus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefBuckets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Gauge refresh:&lt;/strong&gt; run a periodic query to keep &lt;code&gt;outbox_events_pending&lt;/code&gt; accurate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;outbox_events&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;processed_at&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Alert thresholds to consider:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;outbox_events_pending &amp;gt; 1000&lt;/code&gt; for more than two minutes: relay is falling behind or stuck.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;outbox_events_pending&lt;/code&gt; growing monotonically: broker is down or relay has crashed.&lt;/li&gt;
&lt;li&gt;Dead-letter count non-zero: schema or consumer bug needs investigation.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;outbox_batch_duration_seconds p95 &amp;gt; 5s&lt;/code&gt;: database is slow or batch size is too large.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Structured log fields:&lt;/strong&gt; include &lt;code&gt;event_id&lt;/code&gt;, &lt;code&gt;event_type&lt;/code&gt;, &lt;code&gt;aggregate_id&lt;/code&gt;, and &lt;code&gt;attempt&lt;/code&gt; in every log line from the relay. These fields let you correlate a failed publish with the specific outbox row and the downstream consumer trace.&lt;/p&gt;

&lt;h2&gt;
  
  
  Outbox vs. direct queue vs. saga
&lt;/h2&gt;

&lt;p&gt;The outbox pattern is not the right tool for every coordination problem. Here is the comparison:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Atomicity&lt;/th&gt;
&lt;th&gt;Complexity&lt;/th&gt;
&lt;th&gt;When to use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Direct publish&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Acceptable to occasionally lose events&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transactional outbox&lt;/td&gt;
&lt;td&gt;Strong&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Reliable event delivery from a single service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Saga pattern&lt;/td&gt;
&lt;td&gt;Eventual&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Multi-service transactions that span multiple databases&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two-phase commit&lt;/td&gt;
&lt;td&gt;Strong&lt;/td&gt;
&lt;td&gt;Very high&lt;/td&gt;
&lt;td&gt;Rarely practical; avoided in most distributed systems&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The outbox pattern guarantees that a single service reliably emits events that reflect its own state changes. It does not coordinate state changes across multiple services -- that is what the &lt;a href="https://www.glukhov.org/app-architecture/integration-patterns/saga-pattern-distributed-transactions/" rel="noopener noreferrer"&gt;Saga pattern&lt;/a&gt; is for. The choice of broker -- whether &lt;a href="https://www.glukhov.org/data-infrastructure/messaging/rabbitmq-on-eks-vs-sqs/" rel="noopener noreferrer"&gt;RabbitMQ, SQS&lt;/a&gt;, or Kafka -- is independent of the outbox pattern itself; the relay publishes to whichever broker your system uses.&lt;/p&gt;

&lt;p&gt;If you are building a saga, the outbox pattern is still useful: each participant in the saga writes its local state change and its saga event in one transaction using the outbox, then the saga orchestrator or choreography reads those events reliably.&lt;/p&gt;

&lt;h2&gt;
  
  
  WAL-based CDC as an alternative relay
&lt;/h2&gt;

&lt;p&gt;Instead of polling, you can tail PostgreSQL's Write-Ahead Log (WAL) and read outbox inserts directly from the replication stream. Tools like Debezium do this. The advantages are lower latency and no lock pressure on the outbox table. The disadvantages are operational complexity, a dedicated PostgreSQL replication slot, and an external service to run and monitor.&lt;/p&gt;

&lt;p&gt;For most teams, the polling relay described above is the right starting point. WAL tailing makes sense when you have high outbox insert rates (tens of thousands per second), need sub-100ms event latency, or are already running Debezium for other change-capture needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  sqlc integration
&lt;/h2&gt;

&lt;p&gt;If you use &lt;a href="https://www.glukhov.org/app-architecture/data-access/comparing-go-orms-gorm-ent-bun-sqlc/" rel="noopener noreferrer"&gt;sqlc&lt;/a&gt; for type-safe Go database code, the outbox queries fit naturally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- name: InsertOutboxEvent :exec&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;outbox_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aggregate_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;aggregate_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;aggregate_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;aggregate_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- name: FetchOutboxBatch :many&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;aggregate_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;aggregate_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;outbox_events&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;processed_at&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;max_attempts&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;batch_size&lt;/span&gt;
&lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;SKIP&lt;/span&gt; &lt;span class="n"&gt;LOCKED&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- name: MarkOutboxProcessed :exec&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;outbox_events&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;processed_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- name: IncrementOutboxAttempts :exec&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;outbox_events&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- name: OutboxPendingCount :one&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;outbox_events&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;processed_at&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;sqlc generates type-safe functions for each query, which avoids string interpolation errors and keeps the outbox query logic co-located with the rest of your database access layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production checklist
&lt;/h2&gt;

&lt;p&gt;Use this before shipping an outbox implementation:&lt;/p&gt;

&lt;h3&gt;
  
  
  Database
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Outbox table has the partial index on &lt;code&gt;created_at WHERE processed_at IS NULL&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;attempts&lt;/code&gt; column present with a default of 0&lt;/li&gt;
&lt;li&gt;[ ] Dead-letter view or query defined&lt;/li&gt;
&lt;li&gt;[ ] Old processed rows are periodically archived or deleted (a nightly cleanup job suffices)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Relay
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;FOR UPDATE SKIP LOCKED&lt;/code&gt; used in the polling query&lt;/li&gt;
&lt;li&gt;[ ] Relay runs inside a transaction (begin before query, commit after all updates)&lt;/li&gt;
&lt;li&gt;[ ] Batch size is bounded (50-200 rows is typical)&lt;/li&gt;
&lt;li&gt;[ ] Relay respects context cancellation for graceful shutdown&lt;/li&gt;
&lt;li&gt;[ ] Failed publishes increment &lt;code&gt;attempts&lt;/code&gt; rather than causing the batch to abort&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Idempotency
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Published message includes the outbox &lt;code&gt;id&lt;/code&gt; as a deduplication key&lt;/li&gt;
&lt;li&gt;[ ] Consumers are idempotent or the broker provides deduplication&lt;/li&gt;
&lt;li&gt;[ ] See &lt;a href="https://www.glukhov.org/app-architecture/integration-patterns/idempotency-in-distributed-systems/" rel="noopener noreferrer"&gt;Idempotency in Distributed Systems&lt;/a&gt; for deduplication patterns&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Observability
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;outbox_events_pending&lt;/code&gt; gauge is monitored and alerted on&lt;/li&gt;
&lt;li&gt;[ ] Dead-letter count is alerted on&lt;/li&gt;
&lt;li&gt;[ ] Relay batch duration is tracked&lt;/li&gt;
&lt;li&gt;[ ] Structured logs include &lt;code&gt;event_id&lt;/code&gt;, &lt;code&gt;event_type&lt;/code&gt;, and &lt;code&gt;aggregate_id&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Operations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Manual retry path exists for dead-letter rows&lt;/li&gt;
&lt;li&gt;[ ] Relay restart behavior is tested (does it re-publish correctly?)&lt;/li&gt;
&lt;li&gt;[ ] Broker outage behavior is tested (does the outbox grow and drain correctly?)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;The dual-write problem is easy to dismiss as an edge case until it causes an incident. The transactional outbox pattern solves it with tools you already have: a PostgreSQL transaction, a background goroutine, and one extra table. The relay is simple to build, simple to operate, and simple to reason about.&lt;/p&gt;

&lt;p&gt;The cost is that consumers must be designed for at-least-once delivery. That is a reasonable trade-off. Exactly-once delivery across a database and a broker without distributed transactions is not achievable in practice -- and pretending otherwise leads to systems that silently drop or double-process events under failure conditions.&lt;/p&gt;

&lt;p&gt;Write the event with the data. Relay it reliably. Make consumers idempotent. That is the whole pattern.&lt;/p&gt;

&lt;p&gt;This article is part of the &lt;a href="https://www.glukhov.org/app-architecture/" rel="noopener noreferrer"&gt;App Architecture in Production&lt;/a&gt; cluster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/current/sql-select.html#SQL-FOR-UPDATE-SHARE" rel="noopener noreferrer"&gt;PostgreSQL FOR UPDATE SKIP LOCKED&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/current/sql-listen.html" rel="noopener noreferrer"&gt;PostgreSQL LISTEN/NOTIFY&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://debezium.io/documentation/reference/stable/transformations/outbox-event-router.html" rel="noopener noreferrer"&gt;Debezium outbox event router&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pkg.go.dev/github.com/lib/pq#hdr-Notifications" rel="noopener noreferrer"&gt;lib/pq LISTEN support&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pkg.go.dev/database/sql" rel="noopener noreferrer"&gt;database/sql package&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>dev</category>
      <category>go</category>
    </item>
    <item>
      <title>Go context.Context Done Right: Cancellation, Timeouts, and Values</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Sun, 05 Jul 2026 00:39:11 +0000</pubDate>
      <link>https://dev.to/rosgluk/go-contextcontext-done-right-cancellation-timeouts-and-values-310m</link>
      <guid>https://dev.to/rosgluk/go-contextcontext-done-right-cancellation-timeouts-and-values-310m</guid>
      <description>&lt;p&gt;Go's &lt;code&gt;context.Context&lt;/code&gt; is simple enough to use badly — and that is the problem.&lt;/p&gt;

&lt;p&gt;Most Go developers learn the surface rules quickly: pass context as the first argument, check &lt;code&gt;ctx.Done()&lt;/code&gt;, use &lt;code&gt;context.WithTimeout&lt;/code&gt;, and never pass &lt;code&gt;nil&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;DoSomething&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those rules are useful, but they cover the easy part. In production services, context is not just a parameter convention — it is the control plane for request lifetime.&lt;/p&gt;

&lt;p&gt;Context tells work when to stop, how long it has left, which cancellation path was taken, and which request-scoped values need to travel across API boundaries. Used well, it prevents goroutine leaks, avoids wasted work, propagates deadlines, and makes services easier to shut down. Used badly, it becomes a bag of hidden dependencies, fake globals, forgotten timeouts, leaked timers, and confusing cancellation behavior.&lt;/p&gt;

&lt;p&gt;The slightly opinionated version is this: use context for cancellation, deadlines, and request-scoped metadata, and do not use it as a dependency container.&lt;/p&gt;

&lt;h2&gt;
  
  
  What context is for
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;context&lt;/code&gt; package has three main jobs — cancellation, deadlines and timeouts, and request-scoped values — and those three jobs cover everything it is designed to do.&lt;/p&gt;

&lt;p&gt;A context should answer questions like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Has this request been canceled?
How much time does this operation have left?
What request ID should be attached to logs?
Which authenticated user is associated with this request?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A context should not answer questions like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Where is my database connection?
Where is my logger?
Where is my configuration?
Which service implementation should I use?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those are dependencies — pass them explicitly through function parameters (see &lt;a href="https://www.glukhov.org/app-architecture/code-architecture/dependency-injection-in-go/" rel="noopener noreferrer"&gt;Dependency Injection in Go&lt;/a&gt; for patterns on doing this cleanly). Context is for request lifetime and request metadata, not application wiring.&lt;/p&gt;

&lt;h2&gt;
  
  
  The basic context shape
&lt;/h2&gt;

&lt;p&gt;The core interface is small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Context&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Deadline&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deadline&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important parts are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Done()&lt;/code&gt; is closed when the context is canceled or its deadline expires.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Err()&lt;/code&gt; explains why the context ended.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Deadline()&lt;/code&gt; tells you whether the context has a deadline.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Value()&lt;/code&gt; stores request-scoped data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most code does not implement this interface. It receives a context and passes it down.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first rule: pass context explicitly
&lt;/h2&gt;

&lt;p&gt;For functions that do request-scoped or cancelable work, pass context as the first parameter — this is the standard Go convention and what every library and tool in the ecosystem expects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;GetUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do this for functions that may:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Call a database&lt;/li&gt;
&lt;li&gt;Call another service&lt;/li&gt;
&lt;li&gt;Wait on a queue&lt;/li&gt;
&lt;li&gt;Start background work&lt;/li&gt;
&lt;li&gt;Block on I/O&lt;/li&gt;
&lt;li&gt;Use a timeout&lt;/li&gt;
&lt;li&gt;Need request-scoped values&lt;/li&gt;
&lt;li&gt;Need cancellation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not add context to tiny pure functions that do not need it.&lt;/p&gt;

&lt;p&gt;This is fine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;NormalizeEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToLower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not every function needs a context. Adding context everywhere makes code noisy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not store context in structs
&lt;/h2&gt;

&lt;p&gt;Storing a context in a struct is one of the most common mistakes in Go codebases, and it is worth calling out explicitly. Do not do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;UserService&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;  &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do this instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;UserService&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;UserService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;GetUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A context belongs to a request, operation, or task, while a service struct usually lives much longer than any single request. Mixing those lifetimes makes cancellation unclear and makes it hard to reason about which operation a context belongs to.&lt;/p&gt;

&lt;p&gt;There are rare exceptions for types that genuinely represent a single operation lifetime, but they are rare enough that the default rule should be simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Pass context. Do not store it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Do not pass nil context
&lt;/h2&gt;

&lt;p&gt;Never pass &lt;code&gt;nil&lt;/code&gt; as a context.&lt;/p&gt;

&lt;p&gt;Bad:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;svc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DoWork&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;context.Background()&lt;/code&gt; when there is no existing context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;svc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DoWork&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In tests, use the test context when possible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestDoWork&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;svc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DoWork&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A nil context can panic when code calls methods on it. A background context is explicit and safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background, TODO, and request contexts
&lt;/h2&gt;

&lt;p&gt;There are three common starting points.&lt;/p&gt;

&lt;h3&gt;
  
  
  context.Background
&lt;/h3&gt;

&lt;p&gt;Use &lt;code&gt;context.Background()&lt;/code&gt; at the top level of a program when no parent context exists — it is the root context from which all child contexts are derived:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestSomething&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  context.TODO
&lt;/h3&gt;

&lt;p&gt;Use &lt;code&gt;context.TODO()&lt;/code&gt; when you know a context should be used but have not decided which one yet.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TODO&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is useful during migration, but it should not become permanent if a real context exists.&lt;/p&gt;

&lt;h3&gt;
  
  
  Request context
&lt;/h3&gt;

&lt;p&gt;In HTTP servers, use the request context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The request context is canceled when the client connection closes, the request is canceled, or the server finishes handling the request.&lt;/p&gt;

&lt;p&gt;For web services, this is usually the context you should pass down to application code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cancellation with context.WithCancel
&lt;/h2&gt;

&lt;p&gt;Use &lt;code&gt;context.WithCancel&lt;/code&gt; when you want to stop work explicitly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithCancel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The returned &lt;code&gt;cancel&lt;/code&gt; function cancels the child context and releases resources associated with it. Always call it when you are done — even if the context will eventually time out, calling cancel early avoids keeping resources alive longer than necessary.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;RunWorker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithCancel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;done&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;done&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;doBackgroundWork&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}()&lt;/span&gt;

    &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pattern is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Derive a child context.&lt;/li&gt;
&lt;li&gt;Defer cancel.&lt;/li&gt;
&lt;li&gt;Pass the child context to work that should stop together.&lt;/li&gt;
&lt;li&gt;Watch &lt;code&gt;ctx.Done()&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Timeouts with context.WithTimeout
&lt;/h2&gt;

&lt;p&gt;Use &lt;code&gt;context.WithTimeout&lt;/code&gt; when an operation has a maximum duration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example with an HTTP client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;FetchUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewRequestWithContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MethodGet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes the timeout part of the operation, not a hidden global setting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Always call cancel
&lt;/h3&gt;

&lt;p&gt;When you call &lt;code&gt;WithCancel&lt;/code&gt;, &lt;code&gt;WithTimeout&lt;/code&gt;, or &lt;code&gt;WithDeadline&lt;/code&gt;, always call the returned cancel function — this matters for correctness.&lt;/p&gt;

&lt;p&gt;Good:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bad:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Failing to call cancel can keep timers and child contexts alive longer than needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deadlines vs timeouts
&lt;/h2&gt;

&lt;p&gt;A timeout is relative:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A deadline is absolute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithDeadline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most application code uses timeouts. Deadlines are useful when a request has a fixed end time that should be shared across multiple operations — for example, if a request has 900 milliseconds left, do not give each downstream call a fresh 1-second timeout; propagate the remaining budget instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Timeout budgets across service layers
&lt;/h2&gt;

&lt;p&gt;A common mistake is stacking timeouts blindly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DoWork&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;DoWork&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks harmless, but it hides the real budget. The service layer should usually respect the caller's deadline instead of resetting the timer to the same value.&lt;/p&gt;

&lt;p&gt;A better pattern is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DoWork&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c"&gt;// handle error&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then inside the service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;DoWork&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a child timeout only when a sub-operation needs a smaller budget:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;DoWork&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;queryCtx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;500&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Millisecond&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queryCtx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The right mental model is straightforward: the whole request has one outer budget, specific sub-operations may have smaller budgets carved out of that budget, and no layer silently extends the request beyond what the caller intended.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check ctx.Err() to distinguish cancellation from timeout
&lt;/h2&gt;

&lt;p&gt;When a context ends, &lt;code&gt;ctx.Err()&lt;/code&gt; returns the reason.&lt;/p&gt;

&lt;p&gt;Usually it is one of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Canceled&lt;/span&gt;
&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeadlineExceeded&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;resultCh&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lets callers distinguish cancellation from timeout, and that distinction matters in practice. A canceled request often means the client disconnected, while a deadline-exceeded error usually means your service was too slow — they should not always be logged, retried, or reported the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use context.Cause for better cancellation reasons
&lt;/h2&gt;

&lt;p&gt;Modern Go also supports cause-aware cancellation.&lt;/p&gt;

&lt;p&gt;The useful functions include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;context.WithCancelCause&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;context.WithTimeoutCause&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;context.WithDeadlineCause&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;context.Cause&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plain &lt;code&gt;ctx.Err()&lt;/code&gt; tells you the broad reason: canceled or deadline exceeded.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;context.Cause(ctx)&lt;/code&gt; can tell you the more specific cause.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;ErrShutdown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"server shutting down"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithCancelCause&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c"&gt;// Some shutdown signal arrived.&lt;/span&gt;
        &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ErrShutdown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}()&lt;/span&gt;

    &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cause&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use cause-aware cancellation when the reason matters to callers, logs, or cleanup behavior, and avoid it where a plain &lt;code&gt;ctx.Err()&lt;/code&gt; is enough — the extra detail is only worth it when diagnosis genuinely requires it.&lt;/p&gt;

&lt;h2&gt;
  
  
  HTTP server example
&lt;/h2&gt;

&lt;p&gt;A normal HTTP handler should start from &lt;code&gt;r.Context()&lt;/code&gt;. For a full walkthrough of structuring Go HTTP services, see &lt;a href="https://www.glukhov.org/app-architecture/api-architecture/implementing-api-in-go/" rel="noopener noreferrer"&gt;Building REST APIs in Go&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;GetUserHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;svc&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;UserService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandlerFunc&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PathValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;svc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;writeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;writeJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusOK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The service should accept and propagate the context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;UserService&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;UserRepository&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;UserService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;GetUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repository should use context-aware database methods:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;UserRepository&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;UserRepository&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;GetUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;`
        select id, email, name
        from users
        where id = $1
    `&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;

    &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QueryRowContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important thing is the chain — each layer passes the same context down to the next:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A[HTTP request context] --&amp;gt; B[Handler]
    B --&amp;gt; C[Service]
    C --&amp;gt; D[Repository]
    D --&amp;gt; E[Database query]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not break the chain by creating &lt;code&gt;context.Background()&lt;/code&gt; in the middle.&lt;/p&gt;

&lt;h2&gt;
  
  
  The context.Background() mistake: breaking the cancellation chain
&lt;/h2&gt;

&lt;p&gt;This is a common bug:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;UserService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;GetUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This discards all cancellation and deadline information from the caller. If the client disconnects, the database query keeps running. If the request times out, the downstream work may still be in flight. If the server is shutting down, this code ignores it entirely. Replacing the received context with &lt;code&gt;context.Background()&lt;/code&gt; inside business logic is almost always wrong.&lt;/p&gt;

&lt;p&gt;Use the context you were given:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;UserService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;GetUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only use &lt;code&gt;context.Background()&lt;/code&gt; at the edge where no parent context exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  HTTP client example
&lt;/h2&gt;

&lt;p&gt;For outbound HTTP requests, attach the context to the request.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;CallAPI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewRequestWithContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MethodGet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MethodGet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That creates a request without the operation context.&lt;/p&gt;

&lt;p&gt;Also avoid relying only on &lt;code&gt;http.Client.Timeout&lt;/code&gt;. It can be useful as a safety limit, but request contexts give you better propagation across the call chain.&lt;/p&gt;

&lt;p&gt;A common pattern is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;CallAPI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewRequestWithContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MethodGet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use this when the downstream API call has a specific budget inside a larger request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Database example
&lt;/h2&gt;

&lt;p&gt;Most Go database APIs have context-aware methods. For a broader look at how Go data access libraries handle context — including GORM, Ent, Bun, and sqlc — see &lt;a href="https://www.glukhov.org/app-architecture/data-access/comparing-go-orms-gorm-ent-bun-sqlc/" rel="noopener noreferrer"&gt;Comparing Go ORMs for PostgreSQL&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Use them.&lt;/p&gt;

&lt;p&gt;Good:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QueryContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Good:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QueryRowContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Good:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExecContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bad:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The context-aware forms allow database operations to stop when the request is canceled or times out, which is especially important for slow queries, overloaded databases, and user-facing APIs where latency directly affects user experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transactions and context
&lt;/h2&gt;

&lt;p&gt;Transactions need careful context handling.&lt;/p&gt;

&lt;p&gt;A transaction should usually begin with the operation context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BeginTx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Rollback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then use the same context for transaction operations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExecContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Commit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Be careful with timeouts around transactions. If the context is canceled before &lt;code&gt;Commit&lt;/code&gt;, the transaction may be rolled back. That may be what you want, but it should be intentional.&lt;/p&gt;

&lt;p&gt;For long transactions, the better answer is usually not a longer timeout — it is a shorter transaction that does less work per unit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background workers and context
&lt;/h2&gt;

&lt;p&gt;Background workers should receive a context that represents their lifetime.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Worker&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;slog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Logger&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ticker&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewTicker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;ticker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;ticker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doOnce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"worker iteration failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"err"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This worker stops cleanly when the context is canceled, and its ticker is properly cleaned up via &lt;code&gt;defer ticker.Stop()&lt;/code&gt;. In &lt;code&gt;main&lt;/code&gt;, you would create a root context tied to OS signals:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stop&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NotifyContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Interrupt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;syscall&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SIGTERM&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;worker&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;slog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Default&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Canceled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;slog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"worker stopped"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"err"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is context used correctly: it describes the lifetime of the process work, and when the OS sends a signal, the whole tree of goroutines that share this context will stop together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preventing goroutine leaks with context cancellation
&lt;/h2&gt;

&lt;p&gt;A goroutine leak happens when a goroutine remains blocked forever after it is no longer useful.&lt;/p&gt;

&lt;p&gt;Context helps prevent this.&lt;/p&gt;

&lt;p&gt;Bad:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;StartWorker&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;doWork&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This goroutine has no shutdown path.&lt;/p&gt;

&lt;p&gt;Better:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;StartWorker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;ticker&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewTicker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;ticker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;ticker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;doWork&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any goroutine that loops should almost always have a cancellation path.&lt;/p&gt;

&lt;p&gt;That does not mean every goroutine must receive context directly, but the system should have a clear way to stop it.&lt;/p&gt;

&lt;h2&gt;
  
  
  context.AfterFunc
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;context.AfterFunc&lt;/code&gt; runs a function after a context is canceled.&lt;/p&gt;

&lt;p&gt;It can be useful for cleanup, unblocking operations, or bridging APIs that do not natively support context.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;waitWithContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;stop&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AfterFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c"&gt;// Wake up or clean up if needed.&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;AfterFunc&lt;/code&gt; carefully — it starts logic when cancellation happens, which can make control flow harder to follow. For most application code, a normal &lt;code&gt;select&lt;/code&gt; on &lt;code&gt;ctx.Done()&lt;/code&gt; is clearer and easier to reason about. &lt;code&gt;AfterFunc&lt;/code&gt; is most valuable when you need to adapt context cancellation to an API that does not already accept context.&lt;/p&gt;

&lt;h2&gt;
  
  
  context.WithoutCancel
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;context.WithoutCancel&lt;/code&gt; creates a context that is not canceled when the parent is canceled.&lt;/p&gt;

&lt;p&gt;This is useful, but it is also easy to misuse.&lt;/p&gt;

&lt;p&gt;Example use case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audit&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;AuditLog&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandlerFunc&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="c"&gt;// Handle request...&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;

        &lt;span class="n"&gt;auditCtx&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithoutCancel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auditCtx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

            &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;audit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"request completed"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The idea is that the audit write may need to continue briefly even after the request context is canceled. This should be rare and deliberate — do not use &lt;code&gt;WithoutCancel&lt;/code&gt; as a way to avoid dealing with cancellation. Use it only when the child work genuinely must outlive the parent cancellation, and always add a new timeout: a context that ignores cancellation but carries no deadline can easily create background goroutine leaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context values done right
&lt;/h2&gt;

&lt;p&gt;Context values are for request-scoped data that crosses API boundaries.&lt;/p&gt;

&lt;p&gt;Good examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;request ID&lt;/li&gt;
&lt;li&gt;trace ID&lt;/li&gt;
&lt;li&gt;authenticated user ID&lt;/li&gt;
&lt;li&gt;tenant ID&lt;/li&gt;
&lt;li&gt;locale&lt;/li&gt;
&lt;li&gt;security principal&lt;/li&gt;
&lt;li&gt;correlation metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bad examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;database connection&lt;/li&gt;
&lt;li&gt;logger as a hidden dependency&lt;/li&gt;
&lt;li&gt;feature flags for ordinary control flow&lt;/li&gt;
&lt;li&gt;optional function parameters&lt;/li&gt;
&lt;li&gt;configuration&lt;/li&gt;
&lt;li&gt;service clients&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A useful rule: if the value is part of the request's identity or observability context, it may belong in context. If it is a dependency your code needs to do its job, pass it explicitly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use typed keys for context values
&lt;/h2&gt;

&lt;p&gt;Do not use plain strings as context keys.&lt;/p&gt;

&lt;p&gt;Bad:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"userID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"123"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This can collide with other packages.&lt;/p&gt;

&lt;p&gt;Use an unexported custom key type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;userIDKey&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;WithUserID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userIDKey&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;UserIDFromContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userIDKey&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern gives you type safety at the package boundary, avoids key collisions with other packages, and keeps the context API surface clean with typed accessor functions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not use context values for optional parameters
&lt;/h2&gt;

&lt;p&gt;This is bad:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"pageSize"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ListUsers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This hides the function contract.&lt;/p&gt;

&lt;p&gt;Prefer explicit parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ListUsers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ListUsersOptions&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;PageSize&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Context values should not replace function arguments. Hidden input makes code harder to understand, test, and review — and anyone reading the function signature will have no idea the parameter even exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Logging and context
&lt;/h2&gt;

&lt;p&gt;There are two common approaches to logging with context. The examples here use Go's &lt;code&gt;log/slog&lt;/code&gt; package — for a deeper dive into structured logging with slog in production services, see &lt;a href="https://www.glukhov.org/observability/logging/structured-logging-go-slog/" rel="noopener noreferrer"&gt;Structured Logging in Go with slog&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Approach 1: Extract values and attach them to logs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;LogRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;slog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;requestID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;RequestIDFromContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;With&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"request_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requestID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps the logger explicit as a proper dependency and uses context only for request-scoped values that legitimately need to cross API boundaries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Approach 2: Store logger in context
&lt;/h3&gt;

&lt;p&gt;Some codebases store a logger in context.&lt;/p&gt;

&lt;p&gt;This can be convenient, but I do not recommend it as a default. It turns context into a dependency container.&lt;/p&gt;

&lt;p&gt;My preference:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pass logger dependencies explicitly.&lt;/li&gt;
&lt;li&gt;Store trace IDs and request IDs in context.&lt;/li&gt;
&lt;li&gt;Add those values to logs at boundaries or middleware.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This keeps dependencies visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context and tracing
&lt;/h2&gt;

&lt;p&gt;Tracing is one of the strongest use cases for context values, and it is a genuinely good fit. OpenTelemetry and similar systems use context to propagate trace spans across function calls and process boundaries, because trace data is exactly the kind of request-scoped metadata context was designed to carry.&lt;/p&gt;

&lt;p&gt;A typical pattern looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;DoWork&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tracer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Service.DoWork"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;End&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The context carries the active trace span, and the repository can create a child span from it. Each layer adds its own span without any explicit passing of tracer objects — the context does that work transparently across the entire call tree.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error handling with context
&lt;/h2&gt;

&lt;p&gt;When an operation stops because of context cancellation, preserve that information. The patterns here complement the broader error design strategies covered in &lt;a href="https://www.glukhov.org/app-architecture/code-architecture/go-error-handling-architecture/" rel="noopener noreferrer"&gt;Go Error Handling Architecture&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;svc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DoWork&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Canceled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c"&gt;// Client canceled or caller stopped the work.&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeadlineExceeded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c"&gt;// Timeout.&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not blindly wrap context errors in a way that hides them.&lt;/p&gt;

&lt;p&gt;Wrapping with &lt;code&gt;%w&lt;/code&gt; preserves &lt;code&gt;errors.Is&lt;/code&gt;, so callers can still detect cancellation or timeout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"query user: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replacing the error entirely discards that information and breaks any caller that checks for specific context error types:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"query user failed"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Mapping context errors to HTTP responses
&lt;/h2&gt;

&lt;p&gt;Context errors often map to different HTTP outcomes.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;writeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Canceled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="c"&gt;// The client likely went away.&lt;/span&gt;
        &lt;span class="c"&gt;// Some systems log this as a client closed request.&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeadlineExceeded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"request timed out"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusGatewayTimeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"internal server error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusInternalServerError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not treat client cancellation as an application failure — if the user closed the browser tab, that is not your service misbehaving, and logging it as an error adds noise without signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context in middleware
&lt;/h2&gt;

&lt;p&gt;HTTP middleware is a common place to add request-scoped values.&lt;/p&gt;

&lt;p&gt;Example request ID middleware:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;requestIDKey&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;WithRequestID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requestID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requestIDKey&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;requestID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;RequestIDFromContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;requestID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requestIDKey&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;requestID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;RequestIDMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;next&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandlerFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;requestID&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"X-Request-ID"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;requestID&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;requestID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;newRequestID&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;WithRequestID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;requestID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServeHTTP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a good use of context. The request ID belongs to the request, it should travel through the full call chain, and attaching it to logs and traces at every layer is exactly the kind of cross-cutting observability concern that context values are designed to support.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context in tests
&lt;/h2&gt;

&lt;p&gt;In tests, avoid using &lt;code&gt;context.Background()&lt;/code&gt; blindly.&lt;/p&gt;

&lt;p&gt;Prefer &lt;code&gt;t.Context()&lt;/code&gt; when the work belongs to the test lifetime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DoWork&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For timeout behavior, test with a real timeout only if the timeout is small and meaningful.&lt;/p&gt;

&lt;p&gt;For concurrent and time-dependent code, consider using &lt;code&gt;testing/synctest&lt;/code&gt; — &lt;a href="https://www.glukhov.org/app-architecture/testing-architecture/testing-concurrent-go-code-synctest/" rel="noopener noreferrer"&gt;Testing Concurrent Go Code with synctest&lt;/a&gt; covers this tool in depth:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;synctest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;30&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeadlineExceeded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"got %v, want deadline exceeded"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lets you test real timeout values without waiting for real time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context and errgroup
&lt;/h2&gt;

&lt;p&gt;For groups of goroutines that should cancel together, &lt;code&gt;errgroup&lt;/code&gt; is often a good fit.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;FetchAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;errgroup&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;

        &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Go&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If one goroutine returns an error, the group context is canceled and other goroutines that respect &lt;code&gt;ctx.Done()&lt;/code&gt; can stop early. This is far cleaner than manually managing multiple goroutines, channels, and cancellation paths. The key phrase here is "respect the context" — errgroup cannot stop work that ignores &lt;code&gt;ctx.Done()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Graceful shutdown
&lt;/h2&gt;

&lt;p&gt;Context is central to graceful shutdown.&lt;/p&gt;

&lt;p&gt;A typical server setup has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a root context canceled by OS signals&lt;/li&gt;
&lt;li&gt;an HTTP server&lt;/li&gt;
&lt;li&gt;background workers&lt;/li&gt;
&lt;li&gt;a shutdown timeout&lt;/li&gt;
&lt;li&gt;cleanup logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stop&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NotifyContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Interrupt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;syscall&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SIGTERM&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Addr&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;":8080"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Handler&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="n"&gt;shutdownCtx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Shutdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shutdownCtx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;slog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"server shutdown failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"err"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ListenAndServe&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrServerClosed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;slog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"server failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"err"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that the shutdown context is not the same as the root context — the root is already canceled when the OS signal arrives. A separate timeout context gives the shutdown process a bounded amount of time to drain in-flight requests before force-quitting, which is the subtle but important distinction that makes graceful shutdown actually work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common anti-patterns
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Anti-pattern 1: Using context as a dependency container
&lt;/h3&gt;

&lt;p&gt;Bad:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"db"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"logger"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"config"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pass dependencies explicitly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Anti-pattern 2: Creating context.Background inside business logic
&lt;/h3&gt;

&lt;p&gt;Bad:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;DoWork&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This breaks cancellation propagation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Anti-pattern 3: Forgetting cancel
&lt;/h3&gt;

&lt;p&gt;Bad:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Good:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Anti-pattern 4: Putting optional parameters in context
&lt;/h3&gt;

&lt;p&gt;Bad:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"includeDeleted"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use explicit options structs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Anti-pattern 5: Passing context too deep into pure code
&lt;/h3&gt;

&lt;p&gt;Bad:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pure computation does not need context unless it is long-running or cancelable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Anti-pattern 6: Ignoring cancellation in loops
&lt;/h3&gt;

&lt;p&gt;Bad:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Better:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Anti-pattern 7: Swallowing context errors
&lt;/h3&gt;

&lt;p&gt;Bad:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"operation failed"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Good:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"operation failed: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Preserve cancellation and deadline errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical context checklist
&lt;/h2&gt;

&lt;p&gt;Use this checklist for Go backend code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Function signatures
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Context is the first parameter.&lt;/li&gt;
&lt;li&gt;Context is not stored in long-lived structs.&lt;/li&gt;
&lt;li&gt;Context is not passed to pure helper functions unless needed.&lt;/li&gt;
&lt;li&gt;Nil context is never used.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cancellation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Long-running loops check &lt;code&gt;ctx.Done()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Goroutines have a shutdown path.&lt;/li&gt;
&lt;li&gt;Worker lifetimes are tied to a parent context.&lt;/li&gt;
&lt;li&gt;Context cancellation is propagated to downstream calls.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Timeouts
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Outer request timeouts are set at the boundary.&lt;/li&gt;
&lt;li&gt;Sub-operation timeouts are smaller than the outer budget.&lt;/li&gt;
&lt;li&gt;Cancel functions are always called.&lt;/li&gt;
&lt;li&gt;Timeouts are not blindly stacked at every layer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Values
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Context values are request-scoped.&lt;/li&gt;
&lt;li&gt;Keys use custom types, not plain strings.&lt;/li&gt;
&lt;li&gt;Dependencies are not stored in context.&lt;/li&gt;
&lt;li&gt;Optional parameters are not stored in context.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Errors
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;context.Canceled&lt;/code&gt; and &lt;code&gt;context.DeadlineExceeded&lt;/code&gt; are preserved.&lt;/li&gt;
&lt;li&gt;Context errors are mapped correctly at API boundaries.&lt;/li&gt;
&lt;li&gt;Cause-aware cancellation is used only when the reason matters.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Tests
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Tests use &lt;code&gt;t.Context()&lt;/code&gt; where appropriate.&lt;/li&gt;
&lt;li&gt;Timeout tests avoid slow real sleeps.&lt;/li&gt;
&lt;li&gt;Concurrent timeout behavior is tested with &lt;code&gt;testing/synctest&lt;/code&gt; when useful.&lt;/li&gt;
&lt;li&gt;Goroutine leaks are checked by ensuring shutdown paths exist.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to audit context usage in a Go codebase
&lt;/h2&gt;

&lt;p&gt;Search for these patterns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="s2"&gt;"context.Background()"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="s2"&gt;"context.TODO()"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="s2"&gt;"WithTimeout"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="s2"&gt;"WithCancel"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="s2"&gt;"WithValue"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="s2"&gt;"type .* struct"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is &lt;code&gt;context.Background()&lt;/code&gt; only used at top-level boundaries?&lt;/li&gt;
&lt;li&gt;Are cancel functions always called?&lt;/li&gt;
&lt;li&gt;Are timeouts placed at sensible boundaries?&lt;/li&gt;
&lt;li&gt;Are context values really request-scoped?&lt;/li&gt;
&lt;li&gt;Are dependencies hidden in context values?&lt;/li&gt;
&lt;li&gt;Are goroutines stoppable?&lt;/li&gt;
&lt;li&gt;Are context errors preserved?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a good code review habit, because many context bugs are not syntax bugs — they are lifetime bugs that only surface under cancellation, load, or shutdown conditions.&lt;/p&gt;

&lt;h2&gt;
  
  
  My opinionated rules
&lt;/h2&gt;

&lt;p&gt;These rules are boring, but they work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 1: Context is control flow
&lt;/h3&gt;

&lt;p&gt;Use context to control cancellation, deadlines, and request metadata.&lt;/p&gt;

&lt;p&gt;Do not use it to smuggle dependencies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 2: The caller owns the budget
&lt;/h3&gt;

&lt;p&gt;A function should usually respect the context it receives.&lt;/p&gt;

&lt;p&gt;Only create a shorter child timeout when the sub-operation needs a specific smaller budget.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 3: Background belongs at the edge
&lt;/h3&gt;

&lt;p&gt;Use &lt;code&gt;context.Background()&lt;/code&gt; in &lt;code&gt;main&lt;/code&gt;, tests, and top-level setup.&lt;/p&gt;

&lt;p&gt;Do not use it inside service and repository methods to escape cancellation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 4: Values should be boring
&lt;/h3&gt;

&lt;p&gt;Request ID, trace ID, user ID, and tenant ID belong in context. Database connections, loggers, config structs, and service clients do not — they are dependencies and should be passed explicitly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 5: Every goroutine needs a lifetime
&lt;/h3&gt;

&lt;p&gt;If a goroutine starts, you should know exactly how it stops. Context is often the right answer, and if it is not context, there should be some other clear mechanism — a channel, a sync primitive, or an explicit signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;context.Context&lt;/code&gt; is not complicated because the API is large — the API is small. It is complicated because it represents lifetime, and lifetime is architecture. Every decision about where context flows, where it is derived, and where it stops is a decision about how your service handles failure, load, and shutdown.&lt;/p&gt;

&lt;p&gt;A well-used context makes Go services easier to cancel, easier to shut down, easier to observe, and less likely to leak goroutines. A badly used context hides dependencies, discards deadlines, and makes code harder to reason about under pressure.&lt;/p&gt;

&lt;p&gt;The practical takeaway is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pass context down.
Do not store it.
Do not replace explicit parameters with values.
Respect cancellation.
Use timeouts at boundaries.
Always call cancel.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is Go context done right.&lt;/p&gt;

&lt;p&gt;This article is part of the &lt;a href="https://www.glukhov.org/app-architecture/" rel="noopener noreferrer"&gt;App Architecture in Production&lt;/a&gt; cluster, which covers code structure, data access, integration patterns, and testing architecture for production Go and Python systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://pkg.go.dev/context" rel="noopener noreferrer"&gt;https://pkg.go.dev/context&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://go.dev/blog/context" rel="noopener noreferrer"&gt;https://go.dev/blog/context&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://go.dev/blog/context-and-structs" rel="noopener noreferrer"&gt;https://go.dev/blog/context-and-structs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://go.dev/doc/go1.20" rel="noopener noreferrer"&gt;https://go.dev/doc/go1.20&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://go.dev/doc/go1.21" rel="noopener noreferrer"&gt;https://go.dev/doc/go1.21&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pkg.go.dev/net/http" rel="noopener noreferrer"&gt;https://pkg.go.dev/net/http&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pkg.go.dev/database/sql" rel="noopener noreferrer"&gt;https://pkg.go.dev/database/sql&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pkg.go.dev/testing/synctest" rel="noopener noreferrer"&gt;https://pkg.go.dev/testing/synctest&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pkg.go.dev/golang.org/x/sync/errgroup" rel="noopener noreferrer"&gt;https://pkg.go.dev/golang.org/x/sync/errgroup&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>dev</category>
      <category>go</category>
    </item>
    <item>
      <title>What Is Spec-Driven Development? The Spec as Source of Truth</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Sat, 04 Jul 2026 07:48:40 +0000</pubDate>
      <link>https://dev.to/rosgluk/what-is-spec-driven-development-the-spec-as-source-of-truth-4gdd</link>
      <guid>https://dev.to/rosgluk/what-is-spec-driven-development-the-spec-as-source-of-truth-4gdd</guid>
      <description>&lt;p&gt;Spec-Driven Development is one of those ideas that software engineers have reached for before&lt;br&gt;
and then set aside when the effort stopped paying. &lt;/p&gt;

&lt;p&gt;What changed in 2025 is that AI coding&lt;br&gt;
agents arrived and made the absence of explicit intent expensive. Prompts are ephemeral.&lt;br&gt;
Agent sessions reset. Code changes but the reasoning behind it disappears. The spec is the&lt;br&gt;
artifact that stops that from happening.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Spec Is Becoming the Source of Truth
&lt;/h2&gt;

&lt;p&gt;For most of software development history, the spec was either a temporary planning artifact&lt;br&gt;
or an afterthought. Requirements lived in tickets, design decisions lived in chat threads,&lt;br&gt;
and the code was the ground truth. Documentation described what existed after the fact.&lt;/p&gt;

&lt;p&gt;Spec-Driven Development inverts that relationship. The specification becomes the primary&lt;br&gt;
artifact. Code is what gets generated or verified against the spec, not the other way around.&lt;/p&gt;

&lt;p&gt;This is not a new idea. Formal methods, design-by-contract, and BDD all contain versions of&lt;br&gt;
it. What is new is the practical motivation: AI coding agents need explicit, durable context&lt;br&gt;
to produce correct and consistent output. Prompts are too ephemeral. The spec is the only&lt;br&gt;
artifact that can carry intent across agent sessions, across team members, and across time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Spec-Driven Development Actually Means
&lt;/h2&gt;

&lt;p&gt;Spec-Driven Development, usually shortened to SDD, is a workflow where a versioned&lt;br&gt;
specification guides or generates implementation. The specification is written and reviewed&lt;br&gt;
before the agent writes code. It captures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What to build&lt;/strong&gt; -- user problem, goals, and non-goals&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What correct behavior looks like&lt;/strong&gt; -- acceptance criteria, edge cases, error states&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How to build it&lt;/strong&gt; -- architecture decisions, data model, API contracts, security constraints&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How to verify it&lt;/strong&gt; -- test strategy, validation rules, traceability back to requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The spec is not a one-time document. It is updated when reality differs from the design.&lt;br&gt;
When the agent discovers something during implementation that the spec got wrong, the spec&lt;br&gt;
is corrected before continuing. The spec stays honest because it is treated like code.&lt;/p&gt;

&lt;p&gt;Recent academic work formalizes this framing: researchers describe SDD as treating&lt;br&gt;
specifications as the source of truth and code as generated or verified against them. The&lt;br&gt;
practical interpretation is that the spec is the reviewed, durable record of intent that&lt;br&gt;
any human or AI tool can read and trust.&lt;/p&gt;

&lt;p&gt;Three terms capture different points on the spec-use spectrum:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spec-first&lt;/strong&gt; means writing the full specification before any implementation begins. This&lt;br&gt;
is the strictest interpretation and the one closest to waterfall if not done carefully.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spec-anchored&lt;/strong&gt; means keeping a specification in sync with implementation throughout the&lt;br&gt;
feature lifecycle. The spec is updated as decisions change. This is the most practical&lt;br&gt;
version for most teams.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spec-as-source&lt;/strong&gt; means generating or validating implementation from the spec, either&lt;br&gt;
through AI agents or through tooling that checks code against spec constraints. This is the&lt;br&gt;
direction tools like GitHub Spec Kit and Kiro are moving toward.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why SDD Matters Now
&lt;/h2&gt;

&lt;p&gt;The honest answer is that SDD is not compelling for a solo developer building a one-day&lt;br&gt;
script. The overhead is not worth it.&lt;/p&gt;

&lt;p&gt;SDD becomes valuable when three conditions are present: the feature is large enough to span&lt;br&gt;
multiple sessions, the agent needs to make decisions that affect the architecture, and the&lt;br&gt;
work will be reviewed or continued by someone else.&lt;/p&gt;

&lt;p&gt;All three conditions are increasingly common with AI-assisted development.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LLMs need context, not just prompts.&lt;/strong&gt; A model that receives a vague prompt makes vague&lt;br&gt;
decisions. A model that receives a reviewed specification with explicit constraints, non-goals,&lt;br&gt;
and acceptance criteria makes better decisions and is easier to course-correct when it drifts.&lt;br&gt;
This connects to how &lt;a href="https://www.glukhov.org/knowledge-management/foundations/retrieval-vs-representation/" rel="noopener noreferrer"&gt;retrieval and representation&lt;/a&gt; work: giving an agent a versioned spec is a form of structured retrieval of project intent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code generation is cheap; deciding what to build is still hard.&lt;/strong&gt; The bottleneck in AI-assisted&lt;br&gt;
development is no longer typing -- it is knowing what to build and how to constrain the&lt;br&gt;
agent. SDD shifts the effort to where it matters: specifying intent clearly before generation&lt;br&gt;
begins.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompts are ephemeral.&lt;/strong&gt; The agent does not remember what you told it in the last session.&lt;br&gt;
A versioned specification stored in the repository does. Every new session can read the same&lt;br&gt;
spec and implement against the same intent without re-establishing context from scratch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.glukhov.org/ai-devtools/vibe-coding/" rel="noopener noreferrer"&gt;Vibe coding&lt;/a&gt; works until it does not.&lt;/strong&gt; For prototypes and throwaway work, ad-hoc prompting is&lt;br&gt;
faster and appropriate. As soon as a feature requires security constraints, multi-file&lt;br&gt;
architecture decisions, or a team handoff, the absence of a spec becomes the main source of&lt;br&gt;
drift and defects. See the comparison in &lt;a href="https://www.glukhov.org/ai-devtools/vibe-coding/spec-driven-development-vs-vibe-coding/" rel="noopener noreferrer"&gt;SDD vs Vibe Coding&lt;/a&gt; for when each approach applies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core Artifacts
&lt;/h2&gt;

&lt;p&gt;A practical SDD workflow produces four main artifacts. Each one reduces a specific kind of&lt;br&gt;
ambiguity before it reaches the agent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Requirements specification.&lt;/strong&gt; The problem statement, the users affected, the goals, the&lt;br&gt;
explicit non-goals, and the acceptance criteria. Non-goals are as important as goals -- they&lt;br&gt;
tell the agent what not to build and prevent scope creep. Acceptance criteria are precise&lt;br&gt;
enough that each one maps to at least one test.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design specification.&lt;/strong&gt; The architectural decisions relevant to this feature: affected&lt;br&gt;
modules, data model changes, API contracts, migrations, security constraints, observability&lt;br&gt;
requirements, and known failure modes. This is not a full system design document -- it is the&lt;br&gt;
subset of architecture decisions needed to implement this feature correctly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Task plan.&lt;/strong&gt; A sequence of small implementation tasks, each with explicit dependencies,&lt;br&gt;
expected file changes, and validation criteria. Tasks are small enough to implement in a&lt;br&gt;
single agent session and verify with a focused diff. Each task has a human review checkpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Traceability record.&lt;/strong&gt; A mapping from acceptance criteria to tests, from design decisions&lt;br&gt;
to affected files, and from tasks to commits. This is what separates SDD from documentation&lt;br&gt;
that becomes stale: traceability makes it possible to verify that the spec was actually&lt;br&gt;
implemented, not just written.&lt;/p&gt;

&lt;p&gt;These artifacts do not have to be heavyweight. A simple feature might produce a single&lt;br&gt;
two-page markdown document covering all four areas. The format matters less than the habit&lt;br&gt;
of writing intent down before implementation begins.&lt;/p&gt;

&lt;h2&gt;
  
  
  How SDD Differs from Documentation
&lt;/h2&gt;

&lt;p&gt;The most common confusion is treating SDD artifacts as documentation. They are not&lt;br&gt;
documentation in the conventional sense.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Documentation describes.&lt;/strong&gt; It tells you what the system does, how to use it, and what&lt;br&gt;
it contains. It is written after the fact and updated when the system changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Specs constrain.&lt;/strong&gt; A spec tells the agent what it is allowed to build and what it is&lt;br&gt;
not allowed to do. It is authoritative before implementation begins. It is validated after&lt;br&gt;
implementation completes. A spec that describes what was actually built -- rather than&lt;br&gt;
constraining what should be built -- has already failed its purpose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Executable specs guide generation and validation.&lt;/strong&gt; The best SDD specs are close enough&lt;br&gt;
to machine-readable that an agent can implement against them and a test suite can verify&lt;br&gt;
them. Acceptance criteria written as "the endpoint must reject unauthenticated requests with&lt;br&gt;
a 401 response" is an executable spec; "the endpoint is secure" is documentation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.glukhov.org/app-architecture/documentation/decision-records-ai-driven-development/" rel="noopener noreferrer"&gt;Decision Records&lt;/a&gt; -- ADRs, PDRs, and DDRs -- are complementary to SDD artifacts but serve a different&lt;br&gt;
purpose. Decision records capture why a choice was made and what was rejected. SDD&lt;br&gt;
specifications capture what to build and how to verify it. Both belong in the repository.&lt;br&gt;
Together they give AI agents the full picture: the current intent and the reasoning behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How SDD Differs from TDD
&lt;/h2&gt;

&lt;p&gt;Test-Driven Development and Spec-Driven Development are often confused because both produce&lt;br&gt;
explicit artifacts before code exists. The difference is the starting point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TDD starts with tests.&lt;/strong&gt; You write a failing test that describes the behavior you want,&lt;br&gt;
then write the minimum code to make it pass. TDD is a feedback loop at the unit level. It&lt;br&gt;
produces good tests but does not answer the question of whether you are building the right&lt;br&gt;
thing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SDD starts with intent.&lt;/strong&gt; Before tests exist, before the architecture is decided, the&lt;br&gt;
spec answers: who has this problem, what does correct behavior look like, what is explicitly&lt;br&gt;
out of scope. The spec then informs what tests to write, which is why good SDD and good TDD&lt;br&gt;
are complementary rather than competing.&lt;/p&gt;

&lt;p&gt;A practical way to think about it: SDD drives TDD. The acceptance criteria in the spec&lt;br&gt;
become the test scenarios. The design spec identifies the integration boundaries that need&lt;br&gt;
contract tests. The task plan identifies which unit behaviors need test coverage before the&lt;br&gt;
agent implements them.&lt;/p&gt;

&lt;h2&gt;
  
  
  How SDD Differs from BDD
&lt;/h2&gt;

&lt;p&gt;Behavior-Driven Development uses natural-language scenarios -- typically in Gherkin format --&lt;br&gt;
to describe expected behavior from the user's perspective. These scenarios bridge the gap&lt;br&gt;
between business intent and technical implementation.&lt;/p&gt;

&lt;p&gt;SDD is broader. It includes behavior descriptions (which can use BDD-style language or plain&lt;br&gt;
prose) but also covers architecture decisions, data models, security constraints, task&lt;br&gt;
planning, and traceability. BDD can be a useful format for writing acceptance criteria inside&lt;br&gt;
an SDD requirements spec. The spec is the container; BDD scenarios are one way to write what&lt;br&gt;
goes inside it.&lt;/p&gt;

&lt;p&gt;The distinction matters in practice: BDD tooling focuses on making scenarios executable.&lt;br&gt;
SDD practice focuses on making intent durable -- across tools, across sessions, and across&lt;br&gt;
team members.&lt;/p&gt;

&lt;h2&gt;
  
  
  How SDD Differs from Formal Methods
&lt;/h2&gt;

&lt;p&gt;Formal methods use mathematical notation and automated verification to prove properties of&lt;br&gt;
software systems. They are extremely rigorous and extremely expensive for most production&lt;br&gt;
development contexts.&lt;/p&gt;

&lt;p&gt;SDD does not require formal notation. A markdown file with acceptance criteria and architecture&lt;br&gt;
decisions is a specification. It constrains without being mathematically formal. The level of&lt;br&gt;
rigor scales with the stakes: a spec for a billing service should be more precise and more&lt;br&gt;
carefully reviewed than a spec for a documentation page.&lt;/p&gt;

&lt;p&gt;The relationship is a spectrum:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Informal prose spec (minimum viable SDD)&lt;/li&gt;
&lt;li&gt;Structured markdown with acceptance criteria and non-goals&lt;/li&gt;
&lt;li&gt;Machine-readable spec with schema validation&lt;/li&gt;
&lt;li&gt;Contract tests derived directly from the spec&lt;/li&gt;
&lt;li&gt;Formal spec with automated proof&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most teams operate in the middle of that spectrum. The goal is not mathematical rigor -- it&lt;br&gt;
is making intent explicit enough that an AI agent can implement against it and a human&lt;br&gt;
reviewer can verify the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benefits of Spec-Driven Development
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Less intent drift.&lt;/strong&gt; The spec is the reference. When the agent drifts -- and it will -- the&lt;br&gt;
reviewer has something to compare the implementation against. Without a spec, drift is&lt;br&gt;
invisible until something breaks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Better AI outputs.&lt;/strong&gt; Agents given explicit constraints, non-goals, and acceptance criteria&lt;br&gt;
produce implementations that are closer to what was intended and easier to correct when they&lt;br&gt;
miss. Context quality directly determines output quality.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Easier review.&lt;/strong&gt; A pull request attached to a spec is easier to review than a pull request&lt;br&gt;
that requires the reviewer to reconstruct the intent from the code. The spec is the review&lt;br&gt;
checklist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Team alignment.&lt;/strong&gt; When multiple people or agents are working on the same feature, the spec&lt;br&gt;
is the shared contract. Without it, each contributor optimizes locally and the pieces may&lt;br&gt;
not fit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Better test planning.&lt;/strong&gt; Acceptance criteria in the spec map directly to test cases. Test&lt;br&gt;
coverage becomes a spec coverage question: is every acceptance criterion covered by at least&lt;br&gt;
one test?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Durable handoff.&lt;/strong&gt; When a feature changes hands -- between engineers, between agent sessions,&lt;br&gt;
between sprints -- the spec is the handoff artifact. It captures what was decided, what was&lt;br&gt;
out of scope, and what remains to be validated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Costs of Spec-Driven Development
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Upfront effort.&lt;/strong&gt; Writing a good spec before writing any code takes time. For small&lt;br&gt;
features, this overhead is real and sometimes not worth it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;False confidence.&lt;/strong&gt; A spec that exists but is not validated against the implementation&lt;br&gt;
gives a false sense of correctness. Stale specs are sometimes worse than no spec: they&lt;br&gt;
mislead reviewers and agents that read them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stale specs.&lt;/strong&gt; Specs drift when the team treats them as planning artifacts rather than&lt;br&gt;
living documents. Updating the spec when implementation differs from design is not optional&lt;br&gt;
-- it is what separates SDD from documentation that accumulates and rots.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generated bureaucracy.&lt;/strong&gt; AI agents can generate exhaustive task lists and verbose&lt;br&gt;
specifications quickly. A 200-task spec generated in thirty seconds is not a useful spec --&lt;br&gt;
it is a bureaucracy generator. Good SDD requires judgment about what to specify and what to&lt;br&gt;
leave implicit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tool lock-in.&lt;/strong&gt; Some SDD tools are opinionated about format, file structure, and workflow.&lt;br&gt;
A spec written in a proprietary format is harder to carry across tools than a markdown file&lt;br&gt;
with clear headers and acceptance criteria.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Spec-Driven Development is not a new methodology. It is an old discipline becoming practical&lt;br&gt;
again because the cost of implicit intent is now visible in AI-generated code.&lt;/p&gt;

&lt;p&gt;The discipline is simple: write down what you intend to build, reviewed and versioned, before&lt;br&gt;
the agent builds it. Keep that record honest by updating it when reality differs. Use it as&lt;br&gt;
the reference for review, testing, and handoff.&lt;/p&gt;

&lt;p&gt;The spec is not magic. A spec that is not validated becomes the most expensive kind of&lt;br&gt;
documentation: one that misleads confidently. Good SDD is the practice of keeping specs&lt;br&gt;
honest -- small enough to maintain, precise enough to constrain, and durable enough to&lt;br&gt;
outlast any single agent session.&lt;/p&gt;

&lt;h2&gt;
  
  
  Useful Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/app-architecture/documentation/decision-records-ai-driven-development/" rel="noopener noreferrer"&gt;Decision Records for AI-Driven Software Development&lt;/a&gt; -- ADRs, PDRs, and DDRs that complement SDD specs by capturing why decisions were made&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/ai-devtools/vibe-coding/spec-driven-development-vs-vibe-coding/" rel="noopener noreferrer"&gt;Spec-Driven Development vs Vibe Coding: Waterfall?&lt;/a&gt; -- when to add specs and when to keep prompting freely&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/ai-devtools/vibe-coding/" rel="noopener noreferrer"&gt;What is Vibe Coding -- Meaning, Tools, Benefits, and Risks&lt;/a&gt; -- the vibe coding cluster pillar&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/app-architecture/" rel="noopener noreferrer"&gt;App Architecture in Production&lt;/a&gt; -- the cluster home for architecture, documentation, testing, and integration patterns&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/app-architecture/testing-architecture/unit-testing-in-go/" rel="noopener noreferrer"&gt;Unit Testing in Go: Structure and Best Practices&lt;/a&gt; -- turning SDD acceptance criteria into executable tests&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/app-architecture/testing-architecture/unit-testing-in-python/" rel="noopener noreferrer"&gt;Unit Testing in Python: Complete Guide&lt;/a&gt; -- test-writing practices that map to SDD acceptance criteria&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/app-architecture/code-architecture/python-design-patterns-for-clean-architecture/" rel="noopener noreferrer"&gt;Python Design Patterns for Clean Architecture&lt;/a&gt; -- code structure practices that SDD helps preserve&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/knowledge-management/foundations/retrieval-vs-representation/" rel="noopener noreferrer"&gt;Retrieval vs Representation in Knowledge Management&lt;/a&gt; -- how explicit specs relate to AI context and retrieval&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.github.io/spec-kit/" rel="noopener noreferrer"&gt;GitHub Spec Kit documentation&lt;/a&gt; -- a portable open-source SDD toolkit&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://martinfowler.com/articles/exploring-gen-ai/sdd-3-tools.html" rel="noopener noreferrer"&gt;Martin Fowler on Spec-Driven Development tools&lt;/a&gt; -- careful analysis of Kiro, Spec Kit, and Tessl&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aicoding</category>
      <category>architecture</category>
      <category>documentation</category>
      <category>llm</category>
    </item>
    <item>
      <title>Decision Records for AI-Driven Software Development</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Fri, 03 Jul 2026 09:10:32 +0000</pubDate>
      <link>https://dev.to/rosgluk/decision-records-for-ai-driven-software-development-25ba</link>
      <guid>https://dev.to/rosgluk/decision-records-for-ai-driven-software-development-25ba</guid>
      <description>&lt;p&gt;Decision records are the missing memory layer in AI-assisted software development. They capture not just what was built, but why — and that distinction becomes critical when AI tools are writing your code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision records are the missing memory layer
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.glukhov.org/ai-devtools/vibe-coding/" rel="noopener noreferrer"&gt;AI-driven programming&lt;/a&gt; changes the economics of software development by making code cheaper to generate, easier to refactor, and faster to throw away. That is useful. It is also dangerous, because when code becomes easier to produce, the scarce resource is no longer typing — the scarce resource is judgment.&lt;/p&gt;

&lt;p&gt;Why did the team choose PostgreSQL instead of DynamoDB? Why does the product require human review before sending AI-generated emails? Why does the interface show suggestions in a side panel instead of applying them directly? Why was a simpler approach rejected six months ago? The code may show what exists, but it rarely explains why it exists.&lt;/p&gt;

&lt;p&gt;Decision records solve this problem by providing a short, version-controlled document that captures an important choice, the context behind it, the alternatives considered, and the consequences the team accepted. In an AI-assisted codebase, these records become more than documentation — they become durable project memory that both humans and AI coding agents can read before making future changes. The practical operating rule is simple: keep decision records as Markdown files in the repository, review them like code, and let future AI tools read them before proposing or implementing changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are decision records?
&lt;/h2&gt;

&lt;p&gt;A decision record is a written record of a meaningful decision, structured to answer four basic questions: what did we decide, why did we decide it, what alternatives did we consider, and what consequences did we accept? The most common form is the Architecture Decision Record, shortened to ADR. ADRs are widely used to document technical decisions, and the same pattern can be extended beyond architecture into product and design work.&lt;/p&gt;

&lt;p&gt;For AI-driven programming, three types are especially useful:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Record type&lt;/th&gt;
&lt;th&gt;Captures&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ADR&lt;/td&gt;
&lt;td&gt;Architecture and technical decisions&lt;/td&gt;
&lt;td&gt;Use PostgreSQL as the primary database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PDR&lt;/td&gt;
&lt;td&gt;Product behavior and scope decisions&lt;/td&gt;
&lt;td&gt;AI-generated emails must remain drafts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DDR&lt;/td&gt;
&lt;td&gt;Design and interaction decisions&lt;/td&gt;
&lt;td&gt;Show AI suggestions in a side panel&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Together, ADRs, PDRs, and DDRs describe not only the structure of the system but also the intent of the product and the reasoning behind the user experience. That combination matters because AI agents can read code, but code alone does not contain enough context to make good decisions. Decision records give AI systems a reviewed, durable, human-approved source of project intent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture Decision Records
&lt;/h2&gt;

&lt;p&gt;Architecture Decision Records capture technical and structural decisions. Use an ADR when a decision affects the shape of the system — its boundaries, dependencies, operational model, or long-term maintainability.&lt;/p&gt;

&lt;p&gt;Examples of decisions worth recording as ADRs include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Choosing PostgreSQL as the primary database&lt;/li&gt;
&lt;li&gt;Using an event-driven architecture for background processing&lt;/li&gt;
&lt;li&gt;Keeping the application as a modular monolith&lt;/li&gt;
&lt;li&gt;Introducing a message queue&lt;/li&gt;
&lt;li&gt;Choosing REST instead of GraphQL&lt;/li&gt;
&lt;li&gt;Using server-side rendering for the web application&lt;/li&gt;
&lt;li&gt;Requiring all background jobs to be idempotent&lt;/li&gt;
&lt;li&gt;Adopting a specific authentication and authorization model&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An ADR is not a full architecture document — it is intentionally small, recording one important decision at a specific moment in time. A good ADR prevents architectural amnesia: without it, future contributors may rediscover the same tradeoffs, reopen old debates, or accidentally undo important constraints.&lt;/p&gt;

&lt;p&gt;In AI-driven programming, ADRs carry even more weight. AI tools are often skilled at local optimization, and they may propose a technically plausible change that violates a larger architectural constraint. An ADR gives the AI a clear boundary: "This is how this system is supposed to be shaped."&lt;/p&gt;

&lt;h2&gt;
  
  
  Product Decision Records
&lt;/h2&gt;

&lt;p&gt;Product Decision Records capture product behavior, scope, and user-facing intent. This is less common than ADRs, but it is often just as valuable — product decisions are frequently scattered across tickets, roadmap tools, chat threads, meeting notes, and people's memories, making them easy for humans to forget and almost impossible for AI tools to infer reliably.&lt;/p&gt;

&lt;p&gt;Use a PDR when a decision affects what the product does, who it serves, what is intentionally out of scope, or how a user-facing feature should behave. Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI-generated messages must remain drafts until reviewed by a human&lt;/li&gt;
&lt;li&gt;Free-tier users can create up to three projects&lt;/li&gt;
&lt;li&gt;Deleted workspaces are recoverable for 30 days&lt;/li&gt;
&lt;li&gt;Team billing is out of scope for version 1&lt;/li&gt;
&lt;li&gt;Users can export their data without contacting support&lt;/li&gt;
&lt;li&gt;Low-confidence AI summaries show a warning instead of being hidden&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A PDR is especially useful when a product choice looks arbitrary from the code. The code might contain a limit of three projects for free users, and without a PDR an AI tool may treat that number as a magic constant and suggest changing it. With a PDR, the AI can see that the limit is tied to pricing strategy, onboarding cost, or support load — and that changing it requires a deliberate product decision, not a quick edit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design Decision Records
&lt;/h2&gt;

&lt;p&gt;Design Decision Records capture user experience, interaction, visual, and content-design decisions. Use a DDR when a decision affects how users interact with the product, how information is presented, or how a design principle should be applied across future work.&lt;/p&gt;

&lt;p&gt;Examples of design decisions worth recording include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use inline validation instead of validation only on submit&lt;/li&gt;
&lt;li&gt;Put AI suggestions in a side panel instead of directly in the editor&lt;/li&gt;
&lt;li&gt;Use progressive disclosure for advanced settings&lt;/li&gt;
&lt;li&gt;Require confirmation before destructive actions&lt;/li&gt;
&lt;li&gt;Use "Draft" and "Published" instead of "Inactive" and "Active"&lt;/li&gt;
&lt;li&gt;Keep primary actions visible on mobile screens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Design intent is easy to lose during implementation. A developer may simplify a flow, or an AI agent may generate a component that technically works but breaks the intended interaction model. For example, a DDR might record: "We show AI writing suggestions beside the document, not inside it, because users need to compare generated text with their own draft before accepting changes." That record gives future contributors a principle to preserve, not just a layout to copy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why decision records matter more with AI
&lt;/h2&gt;

&lt;p&gt;AI coding tools are powerful, but they are often stateless or only partially aware of project history. They can inspect files, infer patterns, and generate changes — but they do not automatically know which decisions are intentional, which are accidental, and which were already debated and resolved. This creates several distinct risks.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI may reopen settled debates
&lt;/h3&gt;

&lt;p&gt;If the team already decided to use a modular monolith, an AI agent may still propose extracting a service because that looks clean in isolation. Without an ADR, the AI has no durable way to know that the team already considered and rejected that path, and the result is wasted effort or a subtle regression in system coherence.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI may optimize locally and damage globally
&lt;/h3&gt;

&lt;p&gt;A generated refactor may make one file cleaner while violating system boundaries. A UI change may reduce component complexity while weakening the intended user experience. A product change may simplify the implementation while breaking pricing or compliance assumptions. Decision records give the AI a larger frame of reference before it acts on narrowly scoped signals.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI may preserve code but lose intent
&lt;/h3&gt;

&lt;p&gt;A model can follow existing patterns in the codebase, but patterns are not the same as principles. Sometimes existing code is a compromise. Sometimes it is transitional. Sometimes it exists because of an external constraint that is not visible in the file. Decision records explain the difference between "this is how it works" and "this is why it was built this way."&lt;/p&gt;

&lt;h3&gt;
  
  
  AI may generate plausible but wrong rationale
&lt;/h3&gt;

&lt;p&gt;AI can draft decision records, but it can also invent confident-sounding explanations that do not match the real decision. This is why human review is non-negotiable: AI may generate the first draft of a record, but a human must verify that it accurately describes the actual decision, alternatives, and consequences before the record is merged.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision records as part of a broader methodology
&lt;/h2&gt;

&lt;p&gt;Decision records are not just documentation — they are part of a broader way of working that sits at the intersection of lightweight architecture governance, docs as code, &lt;a href="https://www.glukhov.org/knowledge-management/ai-augmented-knowledge/ai-for-knowledge-management-workflows/" rel="noopener noreferrer"&gt;AI-augmented knowledge management workflows&lt;/a&gt;, product discovery, design rationale, AI governance, and code review. A useful way to describe the larger process is Decision-Oriented Development.&lt;/p&gt;

&lt;p&gt;Most AI-driven programming workflows focus narrowly on the generate-review-commit loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
    A[Prompt] --&amp;gt; B[Generate code]
    B --&amp;gt; C[Test]
    C --&amp;gt; D[Commit]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That cycle is too thin for serious systems work. A stronger workflow treats the repository as a store of both code and intent — the diagrams here use &lt;a href="https://www.glukhov.org/documentation-tools/diagrams/mermaid-diagrams-quickstart-cheatsheet/" rel="noopener noreferrer"&gt;Mermaid&lt;/a&gt;, a lightweight format that works well inside Markdown decision records too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TB
    subgraph top[" "]
        direction LR
        A[Frame the problem] --&amp;gt; B[Identify existing decisions] --&amp;gt; C[Explore options and tradeoffs] --&amp;gt; D[Record the selected decision]
    end
    subgraph bottom[" "]
        direction LR
        E[Generate or modify code] --&amp;gt; F[Review code vs decisions] --&amp;gt; G[Merge implementation and memory] --&amp;gt; H[Use record to guide future work]
    end
    D --&amp;gt; E
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This process turns the repository into more than a code store. It becomes the source of truth for implementation, intent, and reasoning — a durable artifact that accumulates value with every decision made.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision records and docs as code
&lt;/h2&gt;

&lt;p&gt;Decision records work best when they follow docs-as-code principles, which means they should be stored in the same repository as the code, written in plain Markdown, reviewed in pull requests, versioned with Git, linked to related issues and pull requests, and searchable by both humans and AI tools. This is far more reliable than storing important decisions in chat, wiki pages, slide decks, or meeting notes — those tools may still be useful for discussion, but the accepted decision should always live close to the code.&lt;/p&gt;

&lt;p&gt;A well-organized repository structure for decision records might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docs/
  decisions/
    architecture/
      0001-use-postgresql-for-primary-storage.md
      0002-keep-billing-inside-the-core-app.md
    product/
      0001-ai-generated-email-requires-human-review.md
      0002-free-tier-project-limit.md
    design/
      0001-use-inline-validation.md
      0002-place-ai-suggestions-in-side-panel.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For smaller projects, a flatter structure works equally well. The exact folder organization matters less than consistency — the records must be easy to find, easy to review, and easy for AI tools to load as context before acting on the codebase. For Go teams, this &lt;code&gt;docs/decisions/&lt;/code&gt; structure fits naturally alongside the &lt;code&gt;cmd/&lt;/code&gt;, &lt;code&gt;internal/&lt;/code&gt;, and &lt;code&gt;api/&lt;/code&gt; layout described in &lt;a href="https://www.glukhov.org/app-architecture/code-architecture/go-project-structure/" rel="noopener noreferrer"&gt;Go Project Structure: Practices &amp;amp; Patterns&lt;/a&gt;, which recommends &lt;code&gt;docs/&lt;/code&gt; as the home for architecture decisions and API references.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical decision record template
&lt;/h2&gt;

&lt;p&gt;A useful decision record template should be short enough that people actually use it. Here is a practical Markdown template that includes an optional but valuable AI guidance section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Decision: Short title&lt;/span&gt;

Status: Proposed | Accepted | Superseded | Deprecated
Date: YYYY-MM-DD
Type: Architecture | Product | Design
Owners: Team or names

&lt;span class="gu"&gt;## Context&lt;/span&gt;

Describe the problem, constraints, goals, user needs, technical facts,
and business factors that led to this decision.

&lt;span class="gu"&gt;## Decision&lt;/span&gt;

State the decision clearly.

&lt;span class="gu"&gt;## Alternatives considered&lt;/span&gt;

&lt;span class="gu"&gt;### Option 1&lt;/span&gt;

Pros:
&lt;span class="p"&gt;-&lt;/span&gt; ...

Cons:
&lt;span class="p"&gt;-&lt;/span&gt; ...

&lt;span class="gu"&gt;## Consequences&lt;/span&gt;

Describe what becomes easier, what becomes harder, and what risks
or follow-up work this creates.

&lt;span class="gu"&gt;## AI guidance&lt;/span&gt;

When an AI assistant works in this area, it should:
&lt;span class="p"&gt;-&lt;/span&gt; Preserve ...
&lt;span class="p"&gt;-&lt;/span&gt; Avoid ...
&lt;span class="p"&gt;-&lt;/span&gt; Prefer ...
&lt;span class="p"&gt;-&lt;/span&gt; Ask for review when ...

&lt;span class="gu"&gt;## Links&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Related issues:
&lt;span class="p"&gt;-&lt;/span&gt; Related pull requests:
&lt;span class="p"&gt;-&lt;/span&gt; Related files:
&lt;span class="p"&gt;-&lt;/span&gt; Supersedes:
&lt;span class="p"&gt;-&lt;/span&gt; Superseded by:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The "AI guidance" section is optional, but for AI-driven programming it is extremely valuable — it turns the decision record into a durable instruction for future agents working in the same area of the codebase.&lt;/p&gt;

&lt;h2&gt;
  
  
  What belongs in a decision record?
&lt;/h2&gt;

&lt;p&gt;Not every choice deserves a record, and if every small implementation detail becomes a decision record the process collapses into noise. Create a decision record when a choice is meaningful and likely to matter later.&lt;/p&gt;

&lt;p&gt;Good candidates are decisions that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Affect multiple parts of the system&lt;/li&gt;
&lt;li&gt;Encode a product promise&lt;/li&gt;
&lt;li&gt;Resolve a real debate&lt;/li&gt;
&lt;li&gt;Introduce a long-term tradeoff&lt;/li&gt;
&lt;li&gt;Depend on business, compliance, or operational constraints&lt;/li&gt;
&lt;li&gt;Would be expensive to rediscover later&lt;/li&gt;
&lt;li&gt;Future AI tools might plausibly get wrong&lt;/li&gt;
&lt;li&gt;Future contributors might be tempted to reverse casually&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Poor candidates include tiny refactor choices, obvious bug fixes, temporary experiments, local naming decisions, and implementation details with no lasting consequence. A good rule of thumb is straightforward: if reversing the decision would require discussion, record the decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Status values and lifecycle
&lt;/h2&gt;

&lt;p&gt;Decision records should have a lifecycle to signal their current standing. The simplest status values are sufficient.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Proposed&lt;/strong&gt; — The decision is being considered but not yet accepted. Use this when the team wants to discuss a decision in a pull request before committing to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Accepted&lt;/strong&gt; — The decision is active and should guide future work. Most useful decision records will spend most of their life in this state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Superseded&lt;/strong&gt; — The decision has been replaced by a newer record. Do not delete old records; keep them for history and link to the newer decision so the evolution of thinking stays visible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deprecated&lt;/strong&gt; — The decision is no longer recommended but may still describe existing parts of the system. This is particularly useful during migrations, when old patterns exist in the codebase alongside newer approaches.&lt;/p&gt;

&lt;p&gt;The important principle is that decision records should be append-friendly. When the team changes direction, create a new record and link the old one rather than rewriting history to make the past look cleaner.&lt;/p&gt;

&lt;h2&gt;
  
  
  How AI should generate decision records
&lt;/h2&gt;

&lt;p&gt;AI can help create decision records, and this is one of the better uses of AI in software development — it is fast at drafting structured documents from context. After a discussion, architecture review, or pull request, you can ask an AI assistant to draft a record:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Draft an Architecture Decision Record for the decision in this pull request.
Include context, alternatives, consequences, and AI guidance.
Save it as Markdown under docs/decisions/architecture.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For product work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Draft a Product Decision Record explaining why AI-generated messages
must remain drafts until reviewed by the user.
Include user impact, out-of-scope behavior, tradeoffs, and AI guidance.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI-generated record should not be trusted automatically, however. Human review should verify that the context is accurate, that the AI has not invented rationale, that the alternatives listed are real, that the consequences are honest, and that the AI guidance matches the team's actual intent. AI is a drafting assistant — it is not the owner of the decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  How AI should read decision records
&lt;/h2&gt;

&lt;p&gt;The other half of the practice is instructing AI to read the records before acting. Before asking an AI assistant to implement a change, include an instruction like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before modifying this feature, read docs/decisions.
Identify any Architecture, Product, or Design Decision Records that apply.
Follow accepted decisions. If your proposed change conflicts with a decision
record, explain the conflict before changing code.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For larger tasks, reinforce the role of the records as project memory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Use the decision records as project memory.
Do not reverse accepted decisions without proposing a new superseding decision.
When you generate code, explain which decision records influenced the implementation.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This changes the AI's role from "predict plausible code" to "operate inside a documented system of constraints" — a significant improvement in reliability for complex or long-lived projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision records in pull requests
&lt;/h2&gt;

&lt;p&gt;Decision records should be part of normal pull request review rather than a separate process. A simple PR checklist entry makes the habit visible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Decision record checklist&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; [ ] This PR does not introduce a significant architecture, product, or design decision.
&lt;span class="p"&gt;-&lt;/span&gt; [ ] This PR introduces a significant decision and includes a new decision record.
&lt;span class="p"&gt;-&lt;/span&gt; [ ] This PR changes a previous decision and includes a superseding record.
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Relevant existing decision records were considered.
&lt;span class="p"&gt;-&lt;/span&gt; [ ] AI-generated code follows the accepted decision records.
&lt;span class="p"&gt;-&lt;/span&gt; [ ] AI-generated decision records were reviewed by a human.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This checklist is simple, but it changes behavior by reminding the team that code is not the only artifact that matters in a pull request. It also makes it natural to catch when an AI-generated change silently violates a prior decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision records and architecture governance
&lt;/h2&gt;

&lt;p&gt;Traditional architecture governance often fails because it is too heavy, too slow, or too disconnected from implementation — central approval boards, large upfront documents, and gatekeeping processes that block rather than guide. Decision records offer a lighter alternative that integrates directly into the development workflow.&lt;/p&gt;

&lt;p&gt;They do not require a central architecture board for every change, nor do they block teams from learning and adapting. Instead, they create a trail of decisions that can be reviewed, referenced, and built on over time. This supports evolutionary architecture: the architecture can change, but it changes with memory rather than in spite of it. The team can revisit old decisions without having to rediscover why they were made, which is a healthier and more honest form of governance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Small records instead of giant documents&lt;/li&gt;
&lt;li&gt;Review near the code instead of separate approval theater&lt;/li&gt;
&lt;li&gt;Historical context instead of tribal knowledge&lt;/li&gt;
&lt;li&gt;Explicit tradeoffs instead of hidden assumptions&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Decision records and product management
&lt;/h2&gt;

&lt;p&gt;Product work also needs decision memory, and this is an area where the value of decision records is often underestimated. A roadmap says what might happen. A ticket says what to build next. Analytics say what users did. None of those fully explain why a product behavior exists.&lt;/p&gt;

&lt;p&gt;Product Decision Records fill that gap and are especially useful for pricing and packaging decisions, permission models, limits and quotas, AI safety and review flows, onboarding choices, user role definitions, collaboration rules, data retention policies, and feature scope boundaries. Once implemented, product decisions become invisible in the code — later, someone sees only the code and asks, "Why does it work this way?" A PDR gives the answer in a form that both humans and AI tools can find and use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision records and design systems
&lt;/h2&gt;

&lt;p&gt;Design systems often document components, tokens, and usage rules, but they rarely document why the system works that way. Design Decision Records fill this gap. A component library might say "use the confirmation dialog for destructive actions," while a DDR explains the rationale: "We require confirmation for destructive actions because users often work with shared team data, and accidental deletion has a high recovery cost."&lt;/p&gt;

&lt;p&gt;That rationale matters beyond the specific component. It helps future designers, developers, and AI tools apply the principle correctly in new situations. Without the DDR, an AI agent may generate a faster interaction that skips confirmation because it appears more efficient. With the DDR, the agent can recognize that preserving the safety property is intentional and non-negotiable.&lt;/p&gt;

&lt;h2&gt;
  
  
  How decision records support spec-driven development
&lt;/h2&gt;

&lt;p&gt;Spec-driven development explains what the system should do. Decision records explain why the team chose that direction, and the distinction matters significantly for AI-assisted work.&lt;/p&gt;

&lt;p&gt;A feature specification may say that AI-generated emails must be saved as drafts. A Product Decision Record explains why automatic sending was rejected, what risks were considered, and which future changes would require a new decision. A design specification may describe a side-panel interaction, while the corresponding DDR explains why inline AI edits were explicitly rejected and why preserving user control was weighted more heavily than workflow speed. An architecture specification may define a service boundary, and its ADR explains why the team chose that boundary over a simpler or more distributed alternative.&lt;/p&gt;

&lt;p&gt;The spec guides implementation. The decision record preserves judgment. Together, they give AI coding agents both instructions and context — the "what" and the "why" — which is what makes the combination so effective for complex, long-lived systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision records are not specifications
&lt;/h2&gt;

&lt;p&gt;Decision records are related to specifications, but they serve a different purpose. A specification says "the system shall do X," while a decision record says "we chose X instead of Y because of these constraints and tradeoffs." That "instead of Y" is the valuable part. AI tools often generate solutions by finding a plausible path to the requested outcome, but decision records tell them which plausible paths have already been explored, evaluated, and rejected — reducing churn and improving the quality of AI-assisted work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision records are not a replacement for tests
&lt;/h2&gt;

&lt;p&gt;Tests verify behavior; decision records explain intent. Both are necessary and they work together. A test can enforce that AI-generated emails must be saved as drafts, while a Product Decision Record explains that this is required because users must review AI-generated communication before it leaves the system. The test protects behavior. The decision record protects meaning. Together, they make future changes safer and more predictable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision records are not a replacement for code comments
&lt;/h2&gt;

&lt;p&gt;Code comments explain local implementation details, while decision records explain broader decisions. Use comments for surprising lines, edge cases, workarounds, and functions that cannot be simplified. Use decision records for why an architecture exists, why a product behavior exists, why an interaction pattern exists, and why the team chose one direction over another. If the explanation affects only a few lines, a comment is the right tool. If it affects the system's direction, a decision record is the right tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Writing records too late
&lt;/h3&gt;

&lt;p&gt;A decision record should be written when the decision is made, not months later when everyone has forgotten the tradeoffs. It is fine to draft one during a pull request. It is better still to draft it before implementation, while the decision is still being actively discussed and the alternatives are fresh.&lt;/p&gt;

&lt;h3&gt;
  
  
  Making records too long
&lt;/h3&gt;

&lt;p&gt;A decision record is not an essay. It should be detailed enough to preserve judgment but short enough that people will actually read it. Prefer clarity over completeness — a concise record that gets read is far more valuable than a comprehensive one that gets skipped.&lt;/p&gt;

&lt;h3&gt;
  
  
  Recording decisions without consequences
&lt;/h3&gt;

&lt;p&gt;The consequences section is the heart of the record. A decision without stated consequences is often just a preference rather than a real decision. Good records admit tradeoffs honestly, including what becomes harder or riskier as a result of the choice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Editing old records as if history changed
&lt;/h3&gt;

&lt;p&gt;When a decision changes, create a new record and mark the old one as superseded. Silently rewriting an old decision to match the current state destroys the historical context that makes decision records valuable. History is useful precisely because it shows how thinking evolved.&lt;/p&gt;

&lt;h3&gt;
  
  
  Letting AI-generated records merge without review
&lt;/h3&gt;

&lt;p&gt;AI can produce a polished, well-structured record that is subtly wrong. Treat AI-generated decision records exactly like AI-generated code — review them carefully, verify the rationale is accurate, and ensure the consequences section reflects what the team actually accepted.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hiding records outside the repository
&lt;/h3&gt;

&lt;p&gt;If decision records live in a separate wiki or documentation system, they are less likely to be updated alongside code changes and far less likely to be read by AI coding tools loading context for a task. Keeping them in the repository is not just a convenience — it is what makes the practice work for AI-assisted development.&lt;/p&gt;

&lt;h2&gt;
  
  
  A lightweight operating model
&lt;/h2&gt;

&lt;p&gt;A practical team process that adds minimal overhead looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;During planning or implementation, identify whether a meaningful decision is being made.&lt;/li&gt;
&lt;li&gt;Ask an AI assistant to draft an ADR, PDR, or DDR based on the discussion.&lt;/li&gt;
&lt;li&gt;Review the draft as a team, verifying context, alternatives, and consequences.&lt;/li&gt;
&lt;li&gt;Commit the record as Markdown in the repository.&lt;/li&gt;
&lt;li&gt;Link it from the related issue or pull request.&lt;/li&gt;
&lt;li&gt;Instruct AI coding tools to read relevant records before making future changes in the area.&lt;/li&gt;
&lt;li&gt;Supersede records when decisions change, preserving the old record for history.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This does not require a new bureaucracy or a dedicated documentation role. It requires a small habit: preserving important judgment at the moment it is created, close to the code where it will be needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example ADR
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Decision: Use PostgreSQL for primary application storage&lt;/span&gt;

Status: Accepted
Date: 2026-06-25
Type: Architecture
Owners: Platform team

&lt;span class="gu"&gt;## Context&lt;/span&gt;

The application needs durable relational storage for accounts, projects,
permissions, and audit events. The team expects frequent reporting queries
and strong consistency requirements for permission checks.

&lt;span class="gu"&gt;## Decision&lt;/span&gt;

We will use PostgreSQL as the primary application database.

&lt;span class="gu"&gt;## Alternatives considered&lt;/span&gt;

&lt;span class="gu"&gt;### DynamoDB&lt;/span&gt;

Pros:
&lt;span class="p"&gt;-&lt;/span&gt; Operationally scalable
&lt;span class="p"&gt;-&lt;/span&gt; Good fit for predictable key-value access patterns

Cons:
&lt;span class="p"&gt;-&lt;/span&gt; More complex for relational queries
&lt;span class="p"&gt;-&lt;/span&gt; Harder for ad hoc reporting
&lt;span class="p"&gt;-&lt;/span&gt; Less familiar to the current team

&lt;span class="gu"&gt;### MySQL&lt;/span&gt;

Pros:
&lt;span class="p"&gt;-&lt;/span&gt; Mature relational database
&lt;span class="p"&gt;-&lt;/span&gt; Familiar operational model

Cons:
&lt;span class="p"&gt;-&lt;/span&gt; PostgreSQL better matches the team's needs for JSON support,
  indexing options, and existing expertise

&lt;span class="gu"&gt;## Consequences&lt;/span&gt;

PostgreSQL becomes a core operational dependency. The team must manage
migrations carefully and monitor query performance. In return, the
application gets strong relational modeling, mature indexing, and
flexible reporting support.

&lt;span class="gu"&gt;## AI guidance&lt;/span&gt;

When modifying persistence code, prefer relational modeling in PostgreSQL.
Do not introduce a second primary database without a superseding ADR.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Example PDR
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Decision: AI-generated emails must remain drafts&lt;/span&gt;

Status: Accepted
Date: 2026-06-25
Type: Product
Owners: Product team

&lt;span class="gu"&gt;## Context&lt;/span&gt;

The product can generate email replies using AI. Sending email is a
high-trust action because mistakes may reach customers, partners, or
internal teams.

&lt;span class="gu"&gt;## Decision&lt;/span&gt;

AI-generated emails must be created as drafts. A human user must
review and send them.

&lt;span class="gu"&gt;## Alternatives considered&lt;/span&gt;

&lt;span class="gu"&gt;### Send automatically&lt;/span&gt;

Pros:
&lt;span class="p"&gt;-&lt;/span&gt; Faster workflow
&lt;span class="p"&gt;-&lt;/span&gt; Less user effort

Cons:
&lt;span class="p"&gt;-&lt;/span&gt; Higher risk of incorrect or inappropriate messages
&lt;span class="p"&gt;-&lt;/span&gt; Lower user trust
&lt;span class="p"&gt;-&lt;/span&gt; Harder to recover from mistakes

&lt;span class="gu"&gt;### Ask for confirmation only after generation&lt;/span&gt;

Pros:
&lt;span class="p"&gt;-&lt;/span&gt; Keeps the workflow simple
&lt;span class="p"&gt;-&lt;/span&gt; Provides some user control

Cons:
&lt;span class="p"&gt;-&lt;/span&gt; Still encourages shallow review
&lt;span class="p"&gt;-&lt;/span&gt; Does not fit existing email client behavior as well as drafts

&lt;span class="gu"&gt;## Consequences&lt;/span&gt;

The workflow is slightly slower, but safer and more trustworthy.
Future automation can improve review speed, but must not bypass
human approval without a superseding PDR.

&lt;span class="gu"&gt;## AI guidance&lt;/span&gt;

When building email-generation features, create drafts by default.
Do not add automatic sending unless a new accepted PDR explicitly allows it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Example DDR
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Decision: Show AI writing suggestions in a side panel&lt;/span&gt;

Status: Accepted
Date: 2026-06-25
Type: Design
Owners: Design team

&lt;span class="gu"&gt;## Context&lt;/span&gt;

Users need help improving written content, but they also need to stay
in control of the final text. Inline AI edits can make it hard to
distinguish user-written content from generated suggestions.

&lt;span class="gu"&gt;## Decision&lt;/span&gt;

AI writing suggestions will appear in a side panel. Users can accept,
reject, or copy suggestions into the main editor.

&lt;span class="gu"&gt;## Alternatives considered&lt;/span&gt;

&lt;span class="gu"&gt;### Apply suggestions inline&lt;/span&gt;

Pros:
&lt;span class="p"&gt;-&lt;/span&gt; Fast
&lt;span class="p"&gt;-&lt;/span&gt; Feels integrated

Cons:
&lt;span class="p"&gt;-&lt;/span&gt; Blurs authorship
&lt;span class="p"&gt;-&lt;/span&gt; Makes review harder
&lt;span class="p"&gt;-&lt;/span&gt; Can surprise users

&lt;span class="gu"&gt;### Show suggestions in a modal&lt;/span&gt;

Pros:
&lt;span class="p"&gt;-&lt;/span&gt; Focused experience
&lt;span class="p"&gt;-&lt;/span&gt; Easy to implement

Cons:
&lt;span class="p"&gt;-&lt;/span&gt; Interrupts writing flow
&lt;span class="p"&gt;-&lt;/span&gt; Harder to compare suggestion and original text

&lt;span class="gu"&gt;## Consequences&lt;/span&gt;

The side panel takes more screen space, especially on small screens.
However, it preserves user control and makes review clearer.

&lt;span class="gu"&gt;## AI guidance&lt;/span&gt;

When adding writing-assistance features, preserve separation between
user text and AI suggestions. Do not apply generated text directly
into the document without explicit user action.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Suggested prompt library
&lt;/h2&gt;

&lt;p&gt;Use these prompts to make decision records part of daily AI-assisted development.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Find relevant records before working on a feature:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Read docs/decisions and identify any accepted decision records that apply
to this task. Summarize the constraints before proposing code changes.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Draft a new ADR:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Draft an Architecture Decision Record for this technical decision.
Include context, decision, alternatives, consequences, and AI guidance.
Keep it concise and specific.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Draft a new PDR:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Draft a Product Decision Record for this product behavior.
Include user impact, scope, alternatives, consequences, and AI guidance.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Draft a new DDR:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Draft a Design Decision Record for this interaction pattern.
Include user problem, alternatives, tradeoffs, consequences, and AI guidance.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Review a pull request against existing decisions:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Review this pull request against the accepted decision records in docs/decisions.
Identify any conflicts, missing decision records, or decisions that should
be superseded.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Supersede a decision:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create a new decision record that supersedes the existing one.
Preserve the historical rationale, explain what changed, and link both records.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Related reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions" rel="noopener noreferrer"&gt;Michael Nygard's original ADR format&lt;/a&gt; — the foundational post that started the ADR movement&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://adr.github.io/" rel="noopener noreferrer"&gt;ADR GitHub organization&lt;/a&gt; — tooling, templates, and community resources for managing decision records&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/app-architecture/" rel="noopener noreferrer"&gt;App Architecture in Production: Integration Patterns, Code Design, and Data Access&lt;/a&gt; — the cluster home covering integration, testing, data access, and software documentation patterns&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/knowledge-management/ai-augmented-knowledge/ai-for-knowledge-management-workflows/" rel="noopener noreferrer"&gt;AI for Knowledge Management: Real Workflows That Hold Up&lt;/a&gt; — practical AI-augmented knowledge workflows that complement the decision-record practice&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>aicoding</category>
      <category>documentation</category>
      <category>markdown</category>
    </item>
    <item>
      <title>Spec-Driven Development vs Vibe Coding: Waterfall?</title>
      <dc:creator>Rost</dc:creator>
      <pubDate>Thu, 02 Jul 2026 10:55:14 +0000</pubDate>
      <link>https://dev.to/rosgluk/spec-driven-development-vs-vibe-coding-waterfall-3e7i</link>
      <guid>https://dev.to/rosgluk/spec-driven-development-vs-vibe-coding-waterfall-3e7i</guid>
      <description>&lt;p&gt;Spec-Driven Development entered 2026 as the serious developer's answer to vibe coding drift.&lt;/p&gt;

&lt;p&gt;The argument is simple: AI agents produce better, more consistent output when they implement&lt;br&gt;
against a reviewed specification rather than an ad-hoc prompt.&lt;br&gt;
Hard to argue with in theory.&lt;/p&gt;

&lt;p&gt;In practice, Hacker News called it "Waterfall Strikes Back."&lt;/p&gt;

&lt;p&gt;Both sides have a point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Case for SDD in a Vibe-Coding World
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.glukhov.org/ai-devtools/vibe-coding/" rel="noopener noreferrer"&gt;Vibe coding&lt;/a&gt; -- the practice of writing a loose prompt and iterating on whatever the AI agent&lt;br&gt;
produces -- works remarkably well for small, exploratory, and throwaway work. For the first&lt;br&gt;
six months of 2025, it was the dominant AI coding pattern. Developers shipped scripts,&lt;br&gt;
prototypes, and simple tools faster than ever.&lt;/p&gt;

&lt;p&gt;Then the projects grew. Multi-file features started drifting. Constraints established in&lt;br&gt;
session one were forgotten by session three. Security assumptions got dropped. Architectural&lt;br&gt;
decisions shifted mid-feature because the agent had no durable memory of intent.&lt;/p&gt;

&lt;p&gt;Spec-Driven Development (SDD) appeared as the disciplined response. The core claim: make the&lt;br&gt;
specification the central artifact, not the prompt. Write requirements, a design, and a task&lt;br&gt;
plan first. Let the agent implement against those artifacts one slice at a time. Keep the spec&lt;br&gt;
versioned and updated.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.glukhov.org/ai-devtools/ai-coding-assistants/" rel="noopener noreferrer"&gt;AI coding tools&lt;/a&gt; -- GitHub Spec Kit, Kiro, BMAD, and a growing set of Claude Code custom workflows -- are all&lt;br&gt;
implementations of this idea. The tooling is real. The interest is real. The backlash is&lt;br&gt;
also real.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Vibe Coding Is Good At
&lt;/h2&gt;

&lt;p&gt;Before dismissing vibe coding, it is worth being precise about what it does well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exploratory prototypes.&lt;/strong&gt; When you are not sure what you want to build, the fastest path&lt;br&gt;
is to build something rough and react to it. SDD requires knowing what to specify. If you&lt;br&gt;
do not know yet, specs are premature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;UI experiments.&lt;/strong&gt; Visual layout and interaction feel are hard to specify in advance. Vibe&lt;br&gt;
coding lets you see options quickly, discard most of them, and converge on something that&lt;br&gt;
actually feels right. A requirements doc does not help you here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Throwaway automation.&lt;/strong&gt; One-off scripts, data extraction jobs, migration helpers -- these&lt;br&gt;
rarely need a design document. The cost of getting it slightly wrong is low. The cost of&lt;br&gt;
a slow, ceremonial process is real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fast feedback.&lt;/strong&gt; When you need to learn something quickly -- does this API work the way&lt;br&gt;
I think it does? -- vibe coding cuts the learning loop to minutes. SDD would slow that down&lt;br&gt;
for no benefit.&lt;/p&gt;

&lt;p&gt;The mistake is to take the success patterns from these contexts and apply them to production&lt;br&gt;
features with real constraints, real users, and real consequences for getting it wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Vibe Coding Breaks Down
&lt;/h2&gt;

&lt;p&gt;Vibe coding degrades predictably as scope and stakes increase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-file changes.&lt;/strong&gt; Once a feature touches five or more files, the agent's context window&lt;br&gt;
starts losing track of invariants. Without a design document, each prompt has to re-establish&lt;br&gt;
context that was established and forgotten in a prior session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Architectural drift.&lt;/strong&gt; Without explicit non-goals, agents implement things. The agent&lt;br&gt;
adds a caching layer because it seems reasonable. Three sessions later, the caching&lt;br&gt;
assumption is baked into the data model and removing it is expensive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forgotten constraints.&lt;/strong&gt; "Only authenticated users can trigger this" is a sentence in a&lt;br&gt;
requirements doc. In a vibe coding session, it is something you mentioned once in session one&lt;br&gt;
and the agent does not remember in session four when it writes the new endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hidden security assumptions.&lt;/strong&gt; Authorization rules, input validation boundaries, secrets&lt;br&gt;
handling -- these are exactly the kind of implicit requirements that get missed when the&lt;br&gt;
agent is optimizing for plausible working code rather than correct, constrained code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Team handoff.&lt;/strong&gt; If you built it through iterative prompting, the artifact that records&lt;br&gt;
what was decided and why is... the git log. Good luck with that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Spec-Driven Development Changes
&lt;/h2&gt;

&lt;p&gt;SDD does not claim to eliminate iteration. The good versions of SDD are explicitly&lt;br&gt;
iterative. What they change is where iteration happens.&lt;/p&gt;

&lt;p&gt;Instead of iterating on code and inferring intent from diffs, you iterate on the spec and&lt;br&gt;
then implement. The spec becomes the artifact that records what was decided, why, and what&lt;br&gt;
is out of scope -- serving a similar function to&lt;br&gt;
&lt;a href="https://www.glukhov.org/app-architecture/documentation/decision-records-ai-driven-development/" rel="noopener noreferrer"&gt;Architecture Decision Records&lt;/a&gt;&lt;br&gt;
but oriented around feature intent rather than system-level choices. The code implements that intent.&lt;/p&gt;

&lt;p&gt;A typical SDD workflow runs through five phases:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Specify.&lt;/strong&gt; Problem statement, users, goals, non-goals, acceptance criteria, open questions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plan.&lt;/strong&gt; Architecture decisions, affected modules, data model, API contracts, security&lt;br&gt;
concerns, test strategy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Task breakdown.&lt;/strong&gt; Small implementation slices with explicit validation criteria and human&lt;br&gt;
review checkpoints.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implement.&lt;/strong&gt; One task at a time, with context reset between tasks, applying constraints&lt;br&gt;
from the spec, and updating the plan when reality differs from the design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Validate.&lt;/strong&gt; Tests, lint, type checks, acceptance criteria, spec-to-code diff.&lt;/p&gt;

&lt;p&gt;The agent participates in most phases -- drafting the spec, generating the design, proposing&lt;br&gt;
tasks -- but humans review the artifacts before implementation starts. That review step is&lt;br&gt;
the central difference between SDD and vibe coding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Developers Call It Waterfall
&lt;/h2&gt;

&lt;p&gt;The waterfall critique is not wrong. It is just aimed at bad SDD, not SDD itself.&lt;/p&gt;

&lt;p&gt;The specific failure mode is long upfront planning. Waterfall's defining characteristic is&lt;br&gt;
a feedback loop that stretches to weeks or months: requirements phase, design phase, build&lt;br&gt;
phase, test phase, ship. Feedback arrives late. By the time you discover that the design&lt;br&gt;
assumption was wrong, you have built on top of it for weeks.&lt;/p&gt;

&lt;p&gt;When a developer uses Spec Kit and generates a 200-line task list before writing a single&lt;br&gt;
line of code, and then spends two days polishing the requirements document before the agent&lt;br&gt;
touches anything, that is waterfall. It is waterfall with markdown instead of UML, but&lt;br&gt;
the failure mode is identical.&lt;/p&gt;

&lt;p&gt;One HN commenter described using Spec Kit for a small CLI tool and finding it "too slow,&lt;br&gt;
too much tweaking before seeing code." That is the bad version. That user was right to&lt;br&gt;
reject it for that task.&lt;/p&gt;

&lt;p&gt;The useful critique is not "specs are bad." It is "long upfront planning before feedback&lt;br&gt;
is bad." Those are different claims.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Useful Middle Ground
&lt;/h2&gt;

&lt;p&gt;Good SDD avoids the waterfall trap by keeping the spec small and starting implementation&lt;br&gt;
early.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Small specs.&lt;/strong&gt; A requirements document for a single feature should fit on one screen.&lt;br&gt;
If the spec is ten pages, it is either a platform design or it needs to be broken into&lt;br&gt;
smaller features. Specs that are too large take too long to review and become stale quickly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Short task slices.&lt;/strong&gt; Each task should be implementable in a single agent session,&lt;br&gt;
reviewable as a small diff, and testable in isolation. If tasks are too large, the&lt;br&gt;
implementation loop stretches and the spec-to-code mapping becomes hard to verify.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Early implementation.&lt;/strong&gt; Spec the first task, implement it, validate it, then move to the&lt;br&gt;
next task. Do not spec everything before implementing anything. The first implementation&lt;br&gt;
will reveal things your spec got wrong. Update the spec before continuing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Living spec.&lt;/strong&gt; When reality differs from the design -- and it will -- update the spec,&lt;br&gt;
not just the code. The spec is only useful if it reflects what was actually built.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tests as executable feedback.&lt;/strong&gt; Every acceptance criterion should map to at least one&lt;br&gt;
test. The test suite is the machine-readable version of the spec. If the spec says "only&lt;br&gt;
authenticated users can trigger this," there should be a test that verifies unauthenticated&lt;br&gt;
requests are rejected.&lt;/p&gt;

&lt;p&gt;This hybrid -- small specs, short tasks, early implementation, living documents -- is what&lt;br&gt;
actually works. It is not vibe coding and it is not waterfall. It is controlled iteration&lt;br&gt;
with durable artifacts.&lt;/p&gt;

&lt;h2&gt;
  
  
  When SDD Beats Vibe Coding
&lt;/h2&gt;

&lt;p&gt;Use SDD -- even lightweight SDD -- when the cost of getting it wrong is real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Risky business logic.&lt;/strong&gt; Billing, permissions, data migrations, idempotency -- any logic&lt;br&gt;
where incorrect behavior is expensive or hard to reverse. Vibe coding leaves these kinds&lt;br&gt;
of requirements implicit. SDD makes them explicit and reviewable before implementation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Production API changes.&lt;/strong&gt; Any change to a public or internal API contract should have a&lt;br&gt;
design document. The design document is what you review before the agent writes code that&lt;br&gt;
breaks callers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-agent workflows.&lt;/strong&gt; When multiple agents are implementing different parts of a&lt;br&gt;
feature, the spec is the shared source of truth. Without it, each agent optimizes locally&lt;br&gt;
and the pieces may not fit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Team handoff.&lt;/strong&gt; If another developer or another agent will continue this work, the spec&lt;br&gt;
is the handoff artifact. A git log and a README are not enough.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Significant refactors.&lt;/strong&gt; Refactors that touch core abstractions need an explicit statement&lt;br&gt;
of what must stay the same (behavior) and what is allowed to change (structure). Without&lt;br&gt;
that, the agent may break contracts that you thought were preserved.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Vibe Coding Is Still Better
&lt;/h2&gt;

&lt;p&gt;SDD is overhead. Sometimes overhead is not worth it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick scripts.&lt;/strong&gt; A 50-line script to rename files or transform JSON does not need a&lt;br&gt;
requirements doc. Write the prompt, check the output, ship it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Experiments.&lt;/strong&gt; If you are learning whether an approach is feasible -- exploring an API,&lt;br&gt;
testing a library, validating a hypothesis -- you need speed, not structure. Experiment&lt;br&gt;
first, specify if the experiment succeeds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;UI sketches.&lt;/strong&gt; Interaction design benefits from seeing rather than specifying. Build&lt;br&gt;
several rough variations quickly, react to what you see, and only spec what you are&lt;br&gt;
actually going to ship.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disposable automation.&lt;/strong&gt; One-time scripts, data imports, migration helpers -- the cost&lt;br&gt;
of a slightly wrong result is usually low, and the artifact is going to be deleted after&lt;br&gt;
use anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solo prototypes.&lt;/strong&gt; If you are the only person who will ever see this code and the goal&lt;br&gt;
is learning rather than production, vibe coding is faster and the downsides are contained.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Simple Decision Framework
&lt;/h2&gt;

&lt;p&gt;The practical question is not "SDD or vibe coding?" It is "how much spec do I need for&lt;br&gt;
this specific task?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use vibe coding when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The task takes less than a day&lt;/li&gt;
&lt;li&gt;You are exploring or learning&lt;/li&gt;
&lt;li&gt;The artifact is throwaway or low-stakes&lt;/li&gt;
&lt;li&gt;You are the only person who will touch this&lt;/li&gt;
&lt;li&gt;Feedback speed matters more than correctness&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use lightweight SDD when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The task takes two or more days&lt;/li&gt;
&lt;li&gt;Multiple files are affected&lt;/li&gt;
&lt;li&gt;There are explicit security or correctness requirements&lt;/li&gt;
&lt;li&gt;Another person or agent will continue the work&lt;/li&gt;
&lt;li&gt;You need to write tests that map to requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use full SDD when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The feature touches a public interface or data contract&lt;/li&gt;
&lt;li&gt;Multiple agents or team members are involved&lt;/li&gt;
&lt;li&gt;The organization requires a design review before implementation&lt;/li&gt;
&lt;li&gt;Compliance or audit trails are required&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most common mistake is applying full SDD to tasks that only need lightweight SDD, and&lt;br&gt;
applying no spec at all to tasks that need at least a lightweight one.&lt;/p&gt;

&lt;p&gt;Bad SDD is waterfall with markdown. Good SDD is controlled iteration with durable artifacts.&lt;br&gt;
Vibe coding is the right tool for the right tasks -- and the wrong tool for the wrong ones.&lt;br&gt;
Knowing the difference is the skill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Useful Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.github.io/spec-kit/" rel="noopener noreferrer"&gt;GitHub Spec Kit documentation&lt;/a&gt; -- the portable SDD toolkit&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://martinfowler.com/articles/exploring-gen-ai/sdd-3-tools.html" rel="noopener noreferrer"&gt;Martin Fowler on SDD tools&lt;/a&gt; -- cautious and useful analysis of Kiro, Spec Kit, and Tessl&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://news.ycombinator.com/item?id=45935763" rel="noopener noreferrer"&gt;HN: Waterfall Strikes Back&lt;/a&gt; -- the original waterfall critique thread&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://news.ycombinator.com/item?id=45154355" rel="noopener noreferrer"&gt;HN: GitHub Spec Kit launch thread&lt;/a&gt; -- community reaction&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/ai-devtools/ai-coding-assistants/" rel="noopener noreferrer"&gt;AI Coding Assistants Comparison&lt;/a&gt; -- tools that support SDD workflows: Cursor, Copilot, Claude Code, Kiro&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/ai-devtools/vibe-coding/" rel="noopener noreferrer"&gt;What is Vibe Coding -- Meaning, Tools, Benefits, and Risks in 2026&lt;/a&gt; -- the full vibe coding cluster pillar&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/ai-devtools/" rel="noopener noreferrer"&gt;AI Developer Tools: The Complete Guide to AI-Powered Development&lt;/a&gt; -- the ai-devtools cluster home&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/app-architecture/documentation/decision-records-ai-driven-development/" rel="noopener noreferrer"&gt;Decision Records for AI-Driven Software Development&lt;/a&gt; -- how to keep architectural intent durable alongside your specs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/ai-devtools/claude-code/claude-skills-for-developers/" rel="noopener noreferrer"&gt;Claude Skills for Developers: SKILL.md for VS Code, JetBrains, Cursor&lt;/a&gt; -- reusable SDD-style workflows in Claude Code&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/app-architecture/code-architecture/python-design-patterns-for-clean-architecture/" rel="noopener noreferrer"&gt;Python Design Patterns for Clean Architecture&lt;/a&gt; -- architecture practices that SDD helps preserve across agent sessions&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.glukhov.org/app-architecture/testing-architecture/unit-testing-in-python/" rel="noopener noreferrer"&gt;Unit Testing in Python: Complete Guide with Examples&lt;/a&gt; -- turning SDD acceptance criteria into executable tests&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aicoding</category>
      <category>llm</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
