<?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: Sri Balaji</title>
    <description>The latest articles on DEV Community by Sri Balaji (@sri2614).</description>
    <link>https://dev.to/sri2614</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%2F467906%2Fee67cb27-7e15-4293-b895-091047dc03fc.jpeg</url>
      <title>DEV Community: Sri Balaji</title>
      <link>https://dev.to/sri2614</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sri2614"/>
    <language>en</language>
    <item>
      <title>Secure API Design: Building APIs That Resist Abuse</title>
      <dc:creator>Sri Balaji</dc:creator>
      <pubDate>Tue, 22 Sep 2026 15:18:26 +0000</pubDate>
      <link>https://dev.to/sri2614/secure-api-design-building-apis-that-resist-abuse-2m19</link>
      <guid>https://dev.to/sri2614/secure-api-design-building-apis-that-resist-abuse-2m19</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚡ &lt;strong&gt;TL;DR:&lt;/strong&gt; The server must trust nothing the client sends. Design APIs that authenticate every route, enforce ownership server-side, and validate input, so an attacker changing one ID in a URL cannot read someone else's data.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The bug that ships in every junior API&lt;/li&gt;
&lt;li&gt;One principle: the server trusts nothing the client sends&lt;/li&gt;
&lt;li&gt;The request gauntlet&lt;/li&gt;
&lt;li&gt;The controls, and what each one stops&lt;/li&gt;
&lt;li&gt;The insecure endpoint vs. the secure one&lt;/li&gt;
&lt;li&gt;Common mistakes that cost hours (or headlines)&lt;/li&gt;
&lt;li&gt;Takeaways&lt;/li&gt;
&lt;li&gt;Where to go next&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The bug that ships in every junior API
&lt;/h2&gt;

&lt;p&gt;You build an invoices API. A logged-in user fetches their bill at &lt;code&gt;GET /api/invoices/1043&lt;/code&gt;. It works, the demo is clean, you ship it. A week later someone is bored and types &lt;code&gt;GET /api/invoices/1044&lt;/code&gt;. They are still logged in as themselves, but they are now reading &lt;em&gt;another customer's&lt;/em&gt; invoice. Name, address, line items, total. Then they write a loop from 1 to 9999 and download everyone's.&lt;/p&gt;

&lt;p&gt;Nothing crashed. No error appeared. The code did exactly what it was told: "find invoice 1044 and return it." The problem is what it was &lt;em&gt;never&lt;/em&gt; told, "...but only if it belongs to the person asking." This is the single most common serious flaw in real APIs, and it is called &lt;strong&gt;IDOR&lt;/strong&gt; (Insecure Direct Object Reference), or &lt;strong&gt;BOLA&lt;/strong&gt; (Broken Object Level Authorization) in the OWASP API Top 10. It sits at number one for a reason.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;Who this is for:&lt;/strong&gt; Developers who can already build a working REST or GraphQL endpoint and now want it to survive a hostile internet. You do not need a security background, if you can read a route handler, you can follow this. We use TypeScript and Python, but the principles are framework-agnostic.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  One principle: the server trusts nothing the client sends
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;The server trusts nothing the client sends. Not the body, not the headers, not the URL, not even the user id the client claims to be.&lt;/p&gt;

&lt;p&gt;The one rule under every control in this article&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every API security control is really one idea applied in a different place. The client is not your code running in a friendly browser, it is &lt;em&gt;anything that can open a socket&lt;/em&gt;: a script, a proxy, curl, a malicious app. The user id, the resource id, the price field, the &lt;code&gt;isAdmin&lt;/code&gt; flag, all of it is just attacker-controllable text until your server independently verifies it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;In the real world&lt;/th&gt;
&lt;th&gt;In tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A boarding pass proves who you are&lt;/td&gt;
&lt;td&gt;Authentication, a verified token identifies the caller&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Your seat assignment is yours, not whoever's number you shout&lt;/td&gt;
&lt;td&gt;Authorization, ownership checks tie a resource to the caller&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security scans every bag, even frequent flyers'&lt;/td&gt;
&lt;td&gt;Input validation, every request body is screened, no exceptions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Only so many people board per minute&lt;/td&gt;
&lt;td&gt;Rate limiting, throughput is capped to stop abuse and scraping&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The jet bridge is sealed end to end&lt;/td&gt;
&lt;td&gt;TLS, the channel is encrypted from client to server&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The same airport security model, mapped onto an HTTP request.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The request gauntlet
&lt;/h2&gt;

&lt;p&gt;A secure request runs a gauntlet. Each stage can reject it, and the order matters: cheap checks (is the channel encrypted? is the caller known? are they over their limit?) run before expensive ones (parse the body, hit the database, check ownership). By the time your handler runs, every assumption has already been verified.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fY2xpZW50KCJDbGllbnQ8YnIvPlVudHJ1c3RlZCIpCiAgY2xhc3Mgbl9jbGllbnQgY2xpZW50OwogIG5fdGxzKCJUTFM8YnIvPkVuY3J5cHQgY2hhbm5lbCIpCiAgY2xhc3Mgbl90bHMgZWRnZTsKICBuX2F1dGhuKCJBdXRoTjxici8-V2hvIGFyZSB5b3U_IikKICBjbGFzcyBuX2F1dGhuIGVkZ2U7CiAgbl9yYXRlKCJSYXRlIGxpbWl0PGJyLz5Ub28gbWFueT8iKQogIGNsYXNzIG5fcmF0ZSBlZGdlOwogIG5fdmFsaWRhdGUoIlZhbGlkYXRpb248YnIvPkJvZHkgd2VsbC1mb3JtZWQ_IikKICBjbGFzcyBuX3ZhbGlkYXRlIGNvbXB1dGU7CiAgbl9hdXRoeigiQXV0aFogKyBvd25lcnNoaXA8YnIvPklzIGl0IHlvdXJzPyIpCiAgY2xhc3Mgbl9hdXRoeiBjb21wdXRlOwogIG5faGFuZGxlcigiSGFuZGxlcjxici8-RG8gdGhlIHdvcmsiKQogIGNsYXNzIG5faGFuZGxlciBjb21wdXRlOwogIG5fZGIoIkRhdGFiYXNlPGJyLz5TY29wZWQgcXVlcnkiKQogIGNsYXNzIG5fZGIgZGF0YTsKICBuX2xvZ3MoIkF1ZGl0IGxvZzxici8-V2hvIGRpZCB3aGF0IikKICBjbGFzcyBuX2xvZ3Mgb2JzZXJ2YWJpbGl0eTsKICBuX2NsaWVudCAtLT58IkhUVFBTInwgbl90bHMKICBuX3RscyAtLT4gbl9hdXRobgogIG5fYXV0aG4gLS0-IG5fcmF0ZQogIG5fcmF0ZSAtLT4gbl92YWxpZGF0ZQogIG5fdmFsaWRhdGUgLS0-IG5fYXV0aHoKICBuX2F1dGh6IC0tPiBuX2hhbmRsZXIKICBuX2hhbmRsZXIgLS0-fCJXSEVSRSBvd25lciA9IG1lInwgbl9kYgogIG5fYXV0aHogLS4tPnwicmVjb3JkInwgbl9sb2dz%3FbgColor%3D0d1017%26type%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fY2xpZW50KCJDbGllbnQ8YnIvPlVudHJ1c3RlZCIpCiAgY2xhc3Mgbl9jbGllbnQgY2xpZW50OwogIG5fdGxzKCJUTFM8YnIvPkVuY3J5cHQgY2hhbm5lbCIpCiAgY2xhc3Mgbl90bHMgZWRnZTsKICBuX2F1dGhuKCJBdXRoTjxici8-V2hvIGFyZSB5b3U_IikKICBjbGFzcyBuX2F1dGhuIGVkZ2U7CiAgbl9yYXRlKCJSYXRlIGxpbWl0PGJyLz5Ub28gbWFueT8iKQogIGNsYXNzIG5fcmF0ZSBlZGdlOwogIG5fdmFsaWRhdGUoIlZhbGlkYXRpb248YnIvPkJvZHkgd2VsbC1mb3JtZWQ_IikKICBjbGFzcyBuX3ZhbGlkYXRlIGNvbXB1dGU7CiAgbl9hdXRoeigiQXV0aFogKyBvd25lcnNoaXA8YnIvPklzIGl0IHlvdXJzPyIpCiAgY2xhc3Mgbl9hdXRoeiBjb21wdXRlOwogIG5faGFuZGxlcigiSGFuZGxlcjxici8-RG8gdGhlIHdvcmsiKQogIGNsYXNzIG5faGFuZGxlciBjb21wdXRlOwogIG5fZGIoIkRhdGFiYXNlPGJyLz5TY29wZWQgcXVlcnkiKQogIGNsYXNzIG5fZGIgZGF0YTsKICBuX2xvZ3MoIkF1ZGl0IGxvZzxici8-V2hvIGRpZCB3aGF0IikKICBjbGFzcyBuX2xvZ3Mgb2JzZXJ2YWJpbGl0eTsKICBuX2NsaWVudCAtLT58IkhUVFBTInwgbl90bHMKICBuX3RscyAtLT4gbl9hdXRobgogIG5fYXV0aG4gLS0-IG5fcmF0ZQogIG5fcmF0ZSAtLT4gbl92YWxpZGF0ZQogIG5fdmFsaWRhdGUgLS0-IG5fYXV0aHoKICBuX2F1dGh6IC0tPiBuX2hhbmRsZXIKICBuX2hhbmRsZXIgLS0-fCJXSEVSRSBvd25lciA9IG1lInwgbl9kYgogIG5fYXV0aHogLS4tPnwicmVjb3JkInwgbl9sb2dz%3FbgColor%3D0d1017%26type%3Dpng" alt="Every request runs the gauntlet left to right. Any stage can reject; the handler only runs once all of them pass." width="1741" height="230"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Every request runs the gauntlet left to right. Any stage can reject; the handler only runs once all of them pass.&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;TLS terminates&lt;/strong&gt;: The request arrives over HTTPS. Plaintext on port 80 is redirected to 443. Nothing, tokens, bodies, cookies, travels in the clear.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authenticate the caller&lt;/strong&gt;: A signed token (session cookie or JWT) is verified. No valid token on a protected route means 401. The server now knows &lt;em&gt;who&lt;/em&gt; is asking, not who the client claims to be.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the rate limit&lt;/strong&gt;: Has this caller exceeded their quota for this window? If so, 429 before any real work happens. This caps brute-force, scraping, and accidental loops.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate the input&lt;/strong&gt;: The body and params are parsed against a strict schema. Unknown fields, wrong types, missing required values, or out-of-range numbers are rejected with 400, before they reach business logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorize the action and the object&lt;/strong&gt;: Two questions: may this &lt;em&gt;role&lt;/em&gt; do this action, and does this &lt;em&gt;specific resource&lt;/em&gt; belong to this caller? Failing either is 403 (or 404 to avoid confirming the resource exists).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run the handler&lt;/strong&gt;: Only now does the handler execute, and it queries the database scoped to the caller's id. Every step gets recorded to an audit log.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The controls, and what each one stops
&lt;/h2&gt;

&lt;p&gt;There is no single "make it secure" switch. Each control closes a specific class of attack. Skip one and you leave that door open, no matter how solid the others are.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Control&lt;/th&gt;
&lt;th&gt;What it stops&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TLS everywhere&lt;/td&gt;
&lt;td&gt;Eavesdropping and tampering, stolen tokens and modified requests on the wire&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authentication on every route&lt;/td&gt;
&lt;td&gt;Anonymous access to protected data; relying on "nobody knows the URL"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Object-level authorization&lt;/td&gt;
&lt;td&gt;IDOR / BOLA, reading or editing other users' objects by guessing ids&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Input validation (allow-list)&lt;/td&gt;
&lt;td&gt;Injection, type-confusion, and oversized payloads reaching your logic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rate limiting&lt;/td&gt;
&lt;td&gt;Brute-force, credential stuffing, scraping, and accidental DoS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No mass assignment&lt;/td&gt;
&lt;td&gt;Privilege escalation by sending fields like &lt;code&gt;role&lt;/code&gt; or &lt;code&gt;isAdmin&lt;/code&gt; in the body&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strict CORS&lt;/td&gt;
&lt;td&gt;Malicious sites making authenticated cross-origin calls on a user's behalf&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generic error responses&lt;/td&gt;
&lt;td&gt;Information leaks, stack traces, SQL, and internal ids handed to attackers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Map each control to the concrete abuse it prevents.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The insecure endpoint vs. the secure one
&lt;/h2&gt;

&lt;p&gt;Here is the invoices endpoint as it usually ships first. It authenticates (you must be logged in) and then... trusts the id from the URL completely. This is the IDOR.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;routes/invoices_insecure.py&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# INSECURE, do not ship this
&lt;/span&gt;&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/invoices/{invoice_id}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_invoice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="c1"&gt;# We checked the user is logged in (authentication).
&lt;/span&gt;    &lt;span class="c1"&gt;# We NEVER checked the invoice belongs to them (authorization).
&lt;/span&gt;    &lt;span class="n"&gt;invoice&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="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM invoices WHERE id = :id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;invoice_id&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Leaks the DB error and the SQL to the client
&lt;/span&gt;        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no invoice &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: query failed&lt;/span&gt;&lt;span class="sh"&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;invoice&lt;/span&gt;  &lt;span class="c1"&gt;# returns ANY user's invoice
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three flaws in nine lines: no ownership check (IDOR), a verbose error that leaks internals, and a query scoped only by id. Now the secure version. The fix is small but the order is everything, validate, then authorize against the &lt;em&gt;caller's&lt;/em&gt; id, then return a generic error.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;routes/invoices.py&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HTTPException&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conint&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InvoicePath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;conint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# input validation: positive int only
&lt;/span&gt;
&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/invoices/{invoice_id}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_invoice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="nc"&gt;InvoicePath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 400 on garbage input
&lt;/span&gt;
    &lt;span class="c1"&gt;# Authorization happens IN the query: scope by the caller's id,
&lt;/span&gt;    &lt;span class="c1"&gt;# not just the resource id. The DB can only ever return
&lt;/span&gt;    &lt;span class="c1"&gt;# invoices this user owns.
&lt;/span&gt;    &lt;span class="n"&gt;invoice&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="nf"&gt;query_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM invoices WHERE id = :id AND owner_id = :uid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uid&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="nb"&gt;id&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;invoice&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# 404, not 403, don't confirm the row exists to a stranger.
&lt;/span&gt;        &lt;span class="c1"&gt;# Generic message: no SQL, no stack trace, no internal ids.
&lt;/span&gt;        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invoice not found&lt;/span&gt;&lt;span class="sh"&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;invoice&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same shape in TypeScript, and this time guarding against &lt;strong&gt;mass assignment&lt;/strong&gt; on an update. Notice we never spread the request body into the database, we pick exactly the fields a user is allowed to set, and we set &lt;code&gt;owner_id&lt;/code&gt; from the verified token, never from the body.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;routes/invoices.ts&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Allow-list: the ONLY fields a client may send. No role, no&lt;/span&gt;
&lt;span class="c1"&gt;// owner_id, no isPaid, those are server-controlled.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;UpdateInvoice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;note&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;dueDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;strict&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// .strict() rejects unknown keys outright&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/invoices/:id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requireAuth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&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="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Bad request&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;UpdateInvoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safeParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Bad request&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Ownership + update in one scoped statement.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;updated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ownerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c1"&gt;// authZ lives here&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                   &lt;span class="c1"&gt;// only allow-listed fields&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invoice not found&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; Push authorization &lt;em&gt;into the query&lt;/em&gt; whenever you can, &lt;code&gt;WHERE owner_id = :me&lt;/code&gt;. A separate "fetch, then check in code" step is one forgotten &lt;code&gt;if&lt;/code&gt; away from an IDOR. If the database can only return your rows, the bug is structurally impossible.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Common mistakes that cost hours (or headlines)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Authorization only in the UI.&lt;/strong&gt; Hiding the "Delete" button does nothing, the endpoint is still one curl away. Every check must live on the server. The client is for convenience, never for security.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IDOR / BOLA.&lt;/strong&gt; Authenticating but not verifying ownership. If &lt;code&gt;GET /orders/123&lt;/code&gt; works for anyone logged in, you are leaking every order. Scope every query by the caller's id.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mass assignment.&lt;/strong&gt; Spreading &lt;code&gt;req.body&lt;/code&gt; straight into a model lets a user send &lt;code&gt;"role": "admin"&lt;/code&gt; or &lt;code&gt;"balance": 999999&lt;/code&gt;. Always allow-list the writable fields explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verbose errors.&lt;/strong&gt; Returning stack traces, SQL, or "user exists but wrong password" hands attackers a map. Log the detail server-side; return a generic message and a correct status code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No rate limiting.&lt;/strong&gt; Without it, login endpoints get credential-stuffed and list endpoints get scraped. Cap requests per caller per window, and return 429 when exceeded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wide-open CORS.&lt;/strong&gt; &lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt; &lt;em&gt;with&lt;/em&gt; credentials lets any site call your API as your logged-in user. Allow-list specific origins; never reflect the request origin blindly.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The whole article in seven lines&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The server trusts nothing the client sends, body, headers, URL, or claimed identity.&lt;/li&gt;
&lt;li&gt;Authenticate on every protected route; "unguessable URL" is not authentication.&lt;/li&gt;
&lt;li&gt;Authorize the object, not just the action: scope every query by the caller's id to kill IDOR/BOLA.&lt;/li&gt;
&lt;li&gt;Validate input with a strict allow-list schema; reject unknown fields and bad types with 400.&lt;/li&gt;
&lt;li&gt;Never spread request bodies into your models, allow-list writable fields to stop mass assignment.&lt;/li&gt;
&lt;li&gt;Rate-limit, lock down CORS to known origins, and return generic errors with correct status codes.&lt;/li&gt;
&lt;li&gt;TLS everywhere. No plaintext, ever.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;Secure API design sits at the intersection of identity, input handling, and transport. Each of those is its own deep topic, and each has a companion article on this site.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/authentication-vs-authorization" rel="noopener noreferrer"&gt;Authentication vs Authorization&lt;/a&gt;, the difference between &lt;em&gt;who you are&lt;/em&gt; and &lt;em&gt;what you may touch&lt;/em&gt;, which is exactly the IDOR fix.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/preventing-injection-attacks-sqli-xss" rel="noopener noreferrer"&gt;Preventing Injection Attacks (SQLi &amp;amp; XSS)&lt;/a&gt;, what rigorous input validation defends against, in depth.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/https-tls-and-encryption-basics" rel="noopener noreferrer"&gt;HTTPS, TLS &amp;amp; Encryption Basics&lt;/a&gt;, how the encrypted channel at the front of the gauntlet actually works.&lt;/li&gt;
&lt;li&gt;Build the muscle memory on the &lt;a href="https://thesimplifiedtech.com/career-paths/devops-engineer" rel="noopener noreferrer"&gt;DevOps Engineer path&lt;/a&gt;, where these controls show up in real pipelines and deploys.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://thesimplifiedtech.com/blog/secure-api-design" rel="noopener noreferrer"&gt;TheSimplifiedTech&lt;/a&gt;, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>devsecops</category>
      <category>cybersecurity</category>
      <category>api</category>
    </item>
    <item>
      <title>Preventing Injection Attacks (SQLi, XSS &amp; Friends)</title>
      <dc:creator>Sri Balaji</dc:creator>
      <pubDate>Mon, 21 Sep 2026 16:43:46 +0000</pubDate>
      <link>https://dev.to/sri2614/preventing-injection-attacks-sqli-xss-friends-2ab9</link>
      <guid>https://dev.to/sri2614/preventing-injection-attacks-sqli-xss-friends-2ab9</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚡ &lt;strong&gt;TL;DR:&lt;/strong&gt; Injection is one bug class wearing many masks: untrusted input treated as code. One rule, never let data become code, kills SQLi, XSS, and command injection at once. Use parameterized queries and allowlists, not blocklists.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A search box that ran DROP TABLE&lt;/li&gt;
&lt;li&gt;The principle: data is not code&lt;/li&gt;
&lt;li&gt;The picture: input crossing the boundary&lt;/li&gt;
&lt;li&gt;The injection family at a glance&lt;/li&gt;
&lt;li&gt;Code: the vulnerable query and the fix&lt;/li&gt;
&lt;li&gt;Why allowlists beat blocklists&lt;/li&gt;
&lt;li&gt;Common mistakes that get people breached&lt;/li&gt;
&lt;li&gt;Takeaways&lt;/li&gt;
&lt;li&gt;Where to go next&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A search box that ran DROP TABLE
&lt;/h2&gt;

&lt;p&gt;A user types into your site's search box. Most people search for &lt;strong&gt;"running shoes"&lt;/strong&gt;. One person types &lt;code&gt;'; DROP TABLE products;--&lt;/code&gt;. A few seconds later, your products table is gone, your store is throwing 500s, and your on-call phone is buzzing. Nobody hacked your server, broke your firewall, or stole a password. They just typed text into a box, and your code ran that text as a &lt;strong&gt;database command&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is injection, and it is the single most common root cause of serious breaches. The mechanism is always the same: your program takes input it does not control and feeds it somewhere that input can become &lt;strong&gt;executable&lt;/strong&gt;, a SQL query, an HTML page, a shell command. The attacker writes the code; your app obediently runs it. The good news is that one mental model defends against the entire family at once.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;Who this is for:&lt;/strong&gt; Junior and mid-level developers who write code that touches a database, renders user content, or shells out to the OS, which is nearly everyone. No security background needed. If you have ever built an &lt;code&gt;WHERE&lt;/code&gt; clause by gluing strings together, this article is for you. It pairs with &lt;a href="https://thesimplifiedtech.com/blog/the-owasp-top-10-explained" rel="noopener noreferrer"&gt;The OWASP Top 10, Explained&lt;/a&gt; and &lt;a href="https://thesimplifiedtech.com/blog/secure-api-design" rel="noopener noreferrer"&gt;Secure API Design&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The principle: data is not code
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Treat all input as hostile. Your job is to make sure data stays data, never let it cross the line and become code.&lt;/p&gt;

&lt;p&gt;The one rule behind every injection fix&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every injection vulnerability is the same bug wearing different clothes. Somewhere, your code builds a string that is part &lt;strong&gt;structure&lt;/strong&gt; (the query, the markup, the command) and part &lt;strong&gt;untrusted value&lt;/strong&gt; (what the user typed). When those two are concatenated into one blob and handed to an interpreter, the interpreter has no way to tell where your intent ends and the attacker's begins. A stray quote, angle bracket, or semicolon flips the value into structure, and now the user is writing your code.&lt;/p&gt;

&lt;p&gt;The fix is not to scrub away every dangerous character. The fix is to &lt;strong&gt;keep data and code in separate channels&lt;/strong&gt; so the interpreter never has to guess. That is what parameterized queries, output encoding, and argument arrays all do, different mechanisms, one idea.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;In the real world&lt;/th&gt;
&lt;th&gt;In tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A mail-merge template: "Dear {name}, your order ships today."&lt;/td&gt;
&lt;td&gt;A query template: SELECT * FROM users WHERE name = ?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The {name} field is filled in from a spreadsheet of addresses.&lt;/td&gt;
&lt;td&gt;The ? placeholder is filled in with the user's input as a value.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Someone writes a macro into the name column, and the merge runs it.&lt;/td&gt;
&lt;td&gt;Concatenated input becomes executable SQL/HTML/shell instead of a value.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fix: the field is always text, never instructions, no matter what's in it.&lt;/td&gt;
&lt;td&gt;Fix: the input is always bound as a value, never parsed as code.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Injection is a mail-merge gone rogue.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The picture: input crossing the boundary
&lt;/h2&gt;

&lt;p&gt;Picture untrusted input arriving at a &lt;strong&gt;trust boundary&lt;/strong&gt;, the edge of your system. Everything past that boundary is your code and your data store. The boundary's job is to decide &lt;em&gt;how&lt;/em&gt; the input is allowed through. Take the safe path and the input is bound, escaped, or rejected; take the unsafe path, string concatenation, and the input flows straight into the interpreter as code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fdXNlcigiVW50cnVzdGVkIGlucHV0PGJyLz5mb3JtLCBVUkwsIGhlYWRlciwgQVBJIGJvZHkiKQogIGNsYXNzIG5fdXNlciBjbGllbnQ7CiAgbl9ib3VuZGFyeSgiVHJ1c3QgYm91bmRhcnk8YnIvPnZhbGlkYXRlIMK3IHBhcmFtZXRlcml6ZSDCtyBlc2NhcGUiKQogIGNsYXNzIG5fYm91bmRhcnkgZWRnZTsKICBuX3ZhbGlkYXRlKCJWYWxpZGF0ZSBpbnB1dDxici8-YWxsb3dsaXN0IHNoYXBlICYgdHlwZSIpCiAgY2xhc3Mgbl92YWxpZGF0ZSBjb21wdXRlOwogIG5fcGFyYW0oIlBhcmFtZXRlcml6ZSAvIGVzY2FwZTxici8-YmluZCBhcyB2YWx1ZSwgZW5jb2RlIG9uIG91dHB1dCIpCiAgY2xhc3Mgbl9wYXJhbSBjb21wdXRlOwogIG5fc2FmZSgiU2FmZSBleGVjdXRpb248YnIvPkRCIC8gcGFnZSAvIHNoZWxsIikKICBjbGFzcyBuX3NhZmUgZGF0YTsKICBuX2V4cGxvaXQoIkV4cGxvaXQ8YnIvPmRhdGEgcmFuIGFzIGNvZGUiKQogIGNsYXNzIG5fZXhwbG9pdCBleHRlcm5hbDsKICBuX3VzZXIgLS0-fCJyZXF1ZXN0Inwgbl9ib3VuZGFyeQogIG5fYm91bmRhcnkgLS0-fCJjaGVjayBzaGFwZSJ8IG5fdmFsaWRhdGUKICBuX2JvdW5kYXJ5IC0tPnwic2FmZSBwYXRoInwgbl9wYXJhbQogIG5fdmFsaWRhdGUgLS0-IG5fcGFyYW0KICBuX3BhcmFtIC0tPnwiZGF0YSBzdGF5cyBkYXRhInwgbl9zYWZlCiAgbl9ib3VuZGFyeSAtLi0-fCJ1bnNhZmU6IHN0cmluZyBjb25jYXQifCBuX2V4cGxvaXQ%3FbgColor%3D0d1017%26type%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fdXNlcigiVW50cnVzdGVkIGlucHV0PGJyLz5mb3JtLCBVUkwsIGhlYWRlciwgQVBJIGJvZHkiKQogIGNsYXNzIG5fdXNlciBjbGllbnQ7CiAgbl9ib3VuZGFyeSgiVHJ1c3QgYm91bmRhcnk8YnIvPnZhbGlkYXRlIMK3IHBhcmFtZXRlcml6ZSDCtyBlc2NhcGUiKQogIGNsYXNzIG5fYm91bmRhcnkgZWRnZTsKICBuX3ZhbGlkYXRlKCJWYWxpZGF0ZSBpbnB1dDxici8-YWxsb3dsaXN0IHNoYXBlICYgdHlwZSIpCiAgY2xhc3Mgbl92YWxpZGF0ZSBjb21wdXRlOwogIG5fcGFyYW0oIlBhcmFtZXRlcml6ZSAvIGVzY2FwZTxici8-YmluZCBhcyB2YWx1ZSwgZW5jb2RlIG9uIG91dHB1dCIpCiAgY2xhc3Mgbl9wYXJhbSBjb21wdXRlOwogIG5fc2FmZSgiU2FmZSBleGVjdXRpb248YnIvPkRCIC8gcGFnZSAvIHNoZWxsIikKICBjbGFzcyBuX3NhZmUgZGF0YTsKICBuX2V4cGxvaXQoIkV4cGxvaXQ8YnIvPmRhdGEgcmFuIGFzIGNvZGUiKQogIGNsYXNzIG5fZXhwbG9pdCBleHRlcm5hbDsKICBuX3VzZXIgLS0-fCJyZXF1ZXN0Inwgbl9ib3VuZGFyeQogIG5fYm91bmRhcnkgLS0-fCJjaGVjayBzaGFwZSJ8IG5fdmFsaWRhdGUKICBuX2JvdW5kYXJ5IC0tPnwic2FmZSBwYXRoInwgbl9wYXJhbQogIG5fdmFsaWRhdGUgLS0-IG5fcGFyYW0KICBuX3BhcmFtIC0tPnwiZGF0YSBzdGF5cyBkYXRhInwgbl9zYWZlCiAgbl9ib3VuZGFyeSAtLi0-fCJ1bnNhZmU6IHN0cmluZyBjb25jYXQifCBuX2V4cGxvaXQ%3FbgColor%3D0d1017%26type%3Dpng" alt="Untrusted input hits a boundary. The safe path keeps data as data; the unsafe path lets it become code." width="1534" height="293"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Untrusted input hits a boundary. The safe path keeps data as data; the unsafe path lets it become code.&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Input arrives&lt;/strong&gt;: A request carries values you did not write, query params, JSON body, headers, cookies. Assume every one is attacker-controlled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hit the boundary&lt;/strong&gt;: Before the value touches any interpreter, route it through validation and a safe-construction mechanism. This is the only place the decision is made.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate the shape&lt;/strong&gt;: Check the value is what you expect, an integer ID, an email, one of a fixed set of sort columns. Reject anything that fails (allowlist), don't try to clean it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bind, don't build&lt;/strong&gt;: Pass the value as a parameter to a prepared statement, or encode it for the exact output context. The interpreter receives structure and data on separate channels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execute safely&lt;/strong&gt;: The query runs with the input as a literal value; the page renders it as visible text; the command treats it as a single argument. Data never became code.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The injection family at a glance
&lt;/h2&gt;

&lt;p&gt;Three interpreters, three contexts, one bug. Notice that the &lt;strong&gt;fix column&lt;/strong&gt; is the same idea each time, separate the data channel from the code channel, in the language of whatever interpreter you are talking to.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Where it hits&lt;/th&gt;
&lt;th&gt;The fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SQL injection (SQLi)&lt;/td&gt;
&lt;td&gt;Database queries built by concatenating input&lt;/td&gt;
&lt;td&gt;Parameterized queries / prepared statements, bind input as values&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-site scripting (XSS)&lt;/td&gt;
&lt;td&gt;HTML/JS rendered with un-encoded input in the browser&lt;/td&gt;
&lt;td&gt;Context-aware output encoding; use textContent, not innerHTML; CSP as defense in depth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Command injection&lt;/td&gt;
&lt;td&gt;OS commands run via a shell with input in the string&lt;/td&gt;
&lt;td&gt;Pass args as an array to the program directly; never invoke a shell with concatenated input&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Same root cause, different interpreter.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code: the vulnerable query and the fix
&lt;/h2&gt;

&lt;p&gt;Here is the classic SQLi bug. The input is glued into the query string, so a crafted value rewrites the query. The infamous &lt;code&gt;' OR '1'='1&lt;/code&gt; turns a login check into "return every row," and &lt;code&gt;'; DROP TABLE ...;--&lt;/code&gt; runs a second statement entirely.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;vulnerable.py&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# DANGER: input is concatenated straight into SQL
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM users WHERE username = &lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"'"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&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="c1"&gt;# Attacker sends:  username = "' OR '1'='1"
# Resulting query: SELECT * FROM users WHERE username = '' OR '1'='1'
#   -&amp;gt; matches every row
#
# Attacker sends:  username = "'; DROP TABLE users;--"
# Resulting query: SELECT * FROM users WHERE username = ''; DROP TABLE users;--'
#   -&amp;gt; runs a second, destructive statement
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is a &lt;strong&gt;parameterized query&lt;/strong&gt;. The &lt;code&gt;?&lt;/code&gt; (or &lt;code&gt;%s&lt;/code&gt;, or &lt;code&gt;$1&lt;/code&gt;, driver-dependent) is a placeholder. You hand the driver the query &lt;em&gt;and&lt;/em&gt; the values as two separate arguments. The driver sends the SQL structure and the data over different channels, so the value is always treated as a literal, even if it contains quotes, semicolons, or whole SQL statements.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;safe.py&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# SAFE: query structure and values travel separately
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM users WHERE username = ?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;   &lt;span class="c1"&gt;# username is bound as a VALUE
&lt;/span&gt;
&lt;span class="c1"&gt;# Attacker sends:  username = "' OR '1'='1"
# The driver looks for a user literally named  ' OR '1'='1
#   -&amp;gt; zero rows. The input never becomes SQL.
#
# Rule of thumb: if you are using string concatenation, +, f-strings,
# or .format() to build SQL, you have a bug. Always parameterize.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;XSS is the same mistake in the browser. If you drop user input into the page as markup, a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; payload executes in your visitors' sessions. The fix is &lt;strong&gt;context-aware output encoding&lt;/strong&gt;: encode the value for the exact place it lands. The simplest version, assign to &lt;code&gt;textContent&lt;/code&gt; (or let a framework escape by default) so the value renders as visible text, never as HTML.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;output-encoding.ts&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// DANGER: input becomes live HTML&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;renderComment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;comment&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// comment = '&amp;lt;img src=x onerror="steal(document.cookie)"&amp;gt;'&lt;/span&gt;
  &lt;span class="c1"&gt;//   -&amp;gt; the onerror handler runs in your user's session&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// SAFE: input is rendered as text, not parsed as markup&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;renderComment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;p&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// &amp;lt; &amp;gt; &amp;amp; are shown literally, never executed&lt;/span&gt;
  &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replaceChildren&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// In React/Vue/Angular, {value} / {{ value }} encode by default.&lt;/span&gt;
&lt;span class="c1"&gt;// The danger is only when you opt out: dangerouslySetInnerHTML, v-html.&lt;/span&gt;
&lt;span class="c1"&gt;// If you MUST render rich HTML, sanitize first (e.g. DOMPurify),&lt;/span&gt;
&lt;span class="c1"&gt;// and add a Content-Security-Policy as defense in depth.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why allowlists beat blocklists
&lt;/h2&gt;

&lt;p&gt;When people first meet injection, the instinct is to &lt;strong&gt;block&lt;/strong&gt; the bad stuff, strip out &lt;code&gt;'&lt;/code&gt;, ban the word &lt;code&gt;DROP&lt;/code&gt;, filter &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;. This is a blocklist, and it loses every time. You are trying to enumerate infinity. Attackers have endless encodings: &lt;code&gt;SeLeCt&lt;/code&gt;, URL-encoding, Unicode homoglyphs, nested tags like &lt;code&gt;&amp;lt;scr&amp;lt;script&amp;gt;ipt&amp;gt;&lt;/code&gt; that survive a single naive strip, comment tricks, and case games. Miss one and you are owned.&lt;/p&gt;

&lt;p&gt;An &lt;strong&gt;allowlist&lt;/strong&gt; flips the logic: define what is &lt;em&gt;valid&lt;/em&gt; and reject everything else. A user ID must match &lt;code&gt;^[0-9]+$&lt;/code&gt;. A sort column must be one of &lt;code&gt;{"name", "created_at", "price"}&lt;/code&gt;. A country must be a known ISO code. You no longer need to imagine every attack, anything that is not explicitly allowed is gone. Validation is about &lt;em&gt;shape and meaning&lt;/em&gt;; it complements (never replaces) parameterizing and encoding.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;allowlist.py&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Allowlist: only known-good values get through.
&lt;/span&gt;&lt;span class="n"&gt;ALLOWED_SORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;created_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;list_products&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sort_by&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;sort_by&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ALLOWED_SORT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="c1"&gt;# reject, don't sanitize
&lt;/span&gt;        &lt;span class="n"&gt;sort_by&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="c1"&gt;# Identifiers (column names) can't be parameterized, so an
&lt;/span&gt;    &lt;span class="c1"&gt;# allowlist is exactly the right tool here.
&lt;/span&gt;    &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM products ORDER BY &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sort_by&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Use both layers:&lt;/strong&gt; Parameterize/encode is your primary defense, it makes injection structurally impossible at the interpreter. Allowlist validation is the second layer, and the only option when you can't parameterize (e.g. column or table names). Defense in depth means an attacker has to beat both.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Common mistakes that get people breached
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;String-concatenated queries.&lt;/strong&gt; Any &lt;code&gt;+&lt;/code&gt;, f-string, or &lt;code&gt;.format()&lt;/code&gt; building SQL is a latent SQLi. Parameterize every value, no exceptions, even "internal" or "admin-only" endpoints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blocklist filtering.&lt;/strong&gt; Stripping &lt;code&gt;'&lt;/code&gt;, banning keywords, or regex-matching "bad" patterns. Attackers route around blocklists with encoding and casing tricks. Allowlist the valid shape instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;innerHTML&lt;/code&gt; with user data.&lt;/strong&gt; Assigning untrusted input to &lt;code&gt;innerHTML&lt;/code&gt; (or &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt; / &lt;code&gt;v-html&lt;/code&gt;) parses it as live markup. Use &lt;code&gt;textContent&lt;/code&gt;, or sanitize with a vetted library if rich HTML is truly required.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trusting client-side validation.&lt;/strong&gt; The browser check is UX, not security, attackers call your API directly and skip it entirely. Always re-validate and parameterize on the server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shelling out with concatenated strings.&lt;/strong&gt; &lt;code&gt;os.system("ping " + host)&lt;/code&gt; is command injection. Pass arguments as an array (&lt;code&gt;subprocess.run(["ping", host])&lt;/code&gt;) so no shell parses the input.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The whole article in seven lines&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Injection = untrusted input getting treated as code. It is the #1 root-cause class.&lt;/li&gt;
&lt;li&gt;One rule fixes the whole family: &lt;strong&gt;never let data become code&lt;/strong&gt;, keep them in separate channels.&lt;/li&gt;
&lt;li&gt;SQLi → parameterized queries. XSS → context-aware output encoding. Command injection → argument arrays, no shell.&lt;/li&gt;
&lt;li&gt;Validate input as an allowlist (known-good shape); reject the rest. Never blocklist.&lt;/li&gt;
&lt;li&gt;Allowlists beat blocklists because you can't enumerate every attack, only every valid value.&lt;/li&gt;
&lt;li&gt;Client-side validation is UX; the server must re-validate and parameterize.&lt;/li&gt;
&lt;li&gt;Layer it: parameterize/encode as the primary defense, allowlist validation as the backstop, CSP as defense in depth.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;Injection is one slice of a bigger picture. See how it ranks and connects in &lt;a href="https://thesimplifiedtech.com/blog/the-owasp-top-10-explained" rel="noopener noreferrer"&gt;The OWASP Top 10, Explained&lt;/a&gt;, then learn where these defenses sit at the edge of a service in &lt;a href="https://thesimplifiedtech.com/blog/secure-api-design" rel="noopener noreferrer"&gt;Secure API Design&lt;/a&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bake validation and parameterization into your service templates from day one, follow the &lt;a href="https://thesimplifiedtech.com/career-paths/devops-engineer" rel="noopener noreferrer"&gt;DevOps Engineer path&lt;/a&gt; to see where security checks fit in the pipeline.&lt;/li&gt;
&lt;li&gt;Practice spotting unsafe string-building in code review, treat every concatenated query, command, or markup string as a finding.&lt;/li&gt;
&lt;li&gt;Add a Content-Security-Policy header to your apps as an XSS backstop, and wire dependency/secret scanning into CI so injection isn't your only line of defense.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://thesimplifiedtech.com/blog/preventing-injection-attacks-sqli-xss" rel="noopener noreferrer"&gt;TheSimplifiedTech&lt;/a&gt;, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>devsecops</category>
      <category>cybersecurity</category>
      <category>injection</category>
    </item>
    <item>
      <title>Secrets Management: Keeping Keys, Passwords, and Tokens Out of the Wrong Hands</title>
      <dc:creator>Sri Balaji</dc:creator>
      <pubDate>Sun, 20 Sep 2026 14:23:47 +0000</pubDate>
      <link>https://dev.to/sri2614/secrets-management-keeping-keys-passwords-and-tokens-out-of-the-wrong-hands-22bf</link>
      <guid>https://dev.to/sri2614/secrets-management-keeping-keys-passwords-and-tokens-out-of-the-wrong-hands-22bf</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚡ &lt;strong&gt;TL;DR:&lt;/strong&gt; Keys, passwords, and tokens never belong in Git, logs, or screenshots. Learn to inject secrets at runtime, store them in a vault, rotate on a schedule with least privilege, and scan to catch leaks before attackers do.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The 3 a.m. cloud bill&lt;/li&gt;
&lt;li&gt;What counts as a secret&lt;/li&gt;
&lt;li&gt;How secrets should actually flow&lt;/li&gt;
&lt;li&gt;Where NOT to put secrets (and where to)&lt;/li&gt;
&lt;li&gt;Reading a secret the right way&lt;/li&gt;
&lt;li&gt;Rotation and least-privilege&lt;/li&gt;
&lt;li&gt;Detecting leaked secrets&lt;/li&gt;
&lt;li&gt;Common mistakes that cost hours (or accounts)&lt;/li&gt;
&lt;li&gt;Takeaways&lt;/li&gt;
&lt;li&gt;Where to go next&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The 3 a.m. cloud bill
&lt;/h2&gt;

&lt;p&gt;A developer pushes a quick fix on a Friday evening. Buried in the diff is a config file with an AWS access key, committed "just to get it working." The repo is public. Within minutes, automated bots scraping GitHub find the key. By Saturday morning the account has spun up hundreds of GPU instances mining cryptocurrency, and the bill reads five figures. The developer did not get hacked through some clever exploit. They handed over the key themselves and pushed it to the internet.&lt;/p&gt;

&lt;p&gt;This is not a rare horror story. Scanners watch public Git pushes in real time, and a leaked credential is often abused &lt;strong&gt;before the developer has even noticed it is gone&lt;/strong&gt;. Secrets are the single highest-leverage thing to protect, because one leaked key can bypass every other control you built.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;Who this is for:&lt;/strong&gt; Developers and ops folks who have ever pasted an API key into a config file, dropped a password into a &lt;code&gt;.env&lt;/code&gt;, or wondered where credentials are &lt;em&gt;supposed&lt;/em&gt; to live. No prior security background needed, we start from "what is a secret" and end with rotation and leak detection.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What counts as a secret
&lt;/h2&gt;

&lt;p&gt;A secret is any value that grants access or proves identity and would cause harm if a stranger held it: API keys, database passwords, OAuth tokens, TLS private keys, signing keys, SSH keys, webhook signing secrets. The defining trait is simple, &lt;strong&gt;if it leaks, someone can act as you.&lt;/strong&gt; Treat anything matching that test as a secret, not as ordinary config.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A secret is only secret while exactly the systems that need it can read it, and nothing else can.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That sentence is the whole discipline. Everything below, where you store secrets, how you hand them to apps, how often you change them, who can read them, is just making that one sentence true and keeping it true.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;In the real world&lt;/th&gt;
&lt;th&gt;In tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;You don't tape your house key to the front door&lt;/td&gt;
&lt;td&gt;Don't hardcode credentials in source or commit them to Git, that's leaving the key on the lock&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You don't make 100 copies and hand them out&lt;/td&gt;
&lt;td&gt;Grant least-privilege access, each service gets only the secrets it needs, nothing more&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;If a key is lost, you change the locks&lt;/td&gt;
&lt;td&gt;Rotate the secret immediately on suspected leak; the old value stops working&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A locksmith keeps the master copies in a safe&lt;/td&gt;
&lt;td&gt;A secret manager (Vault, AWS Secrets Manager, KMS) is the safe, one guarded source of truth&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Secrets behave exactly like the keys to your house.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How secrets should actually flow
&lt;/h2&gt;

&lt;p&gt;The goal is that a secret never lives in your codebase, your container image, or your Git history. Instead the app fetches it at &lt;strong&gt;startup or runtime&lt;/strong&gt; from a dedicated secret manager, holds it in memory, and never writes it down. Behind the scenes a rotation loop keeps swapping the stored value for a fresh one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fYXBwKCJBcHAgc3RhcnRzPGJyLz5ObyBzZWNyZXRzIGJha2VkIGluIikKICBjbGFzcyBuX2FwcCBjb21wdXRlOwogIG5fYXV0aCgiQXV0aGVudGljYXRlPGJyLz5Xb3JrbG9hZCBpZGVudGl0eSAvIElBTSByb2xlIikKICBjbGFzcyBuX2F1dGggZWRnZTsKICBuX3ZhdWx0KCJTZWNyZXQgbWFuYWdlcjxici8-VmF1bHQgLyBBV1MgU00gLyBLTVMiKQogIGNsYXNzIG5fdmF1bHQgZGF0YTsKICBuX2luamVjdCgiSW5qZWN0IGF0IHJ1bnRpbWU8YnIvPkludG8gbWVtb3J5IC8gZW52IikKICBjbGFzcyBuX2luamVjdCBjb21wdXRlOwogIG5fdXNlKCJVc2Ugc2VjcmV0PGJyLz5DYWxsIERCIC8gQVBJIikKICBjbGFzcyBuX3VzZSBleHRlcm5hbDsKICBuX3JvdGF0ZSgiUm90YXRpb24gbG9vcDxici8-SXNzdWUgbmV3LCBleHBpcmUgb2xkIikKICBjbGFzcyBuX3JvdGF0ZSBvYnNlcnZhYmlsaXR5OwogIG5fYXVkaXQoIkF1ZGl0IGxvZzxici8-V2hvIHJlYWQgd2hhdCwgd2hlbiIpCiAgY2xhc3Mgbl9hdWRpdCBvYnNlcnZhYmlsaXR5OwogIG5fYXBwIC0tPnwiaWRlbnRpdHkifCBuX2F1dGgKICBuX2F1dGggLS0-fCJ2ZXJpZnkifCBuX3ZhdWx0CiAgbl92YXVsdCAtLT58InNlY3JldCJ8IG5faW5qZWN0CiAgbl9pbmplY3QgLS0-IG5fdXNlCiAgbl9yb3RhdGUgLS4tPnwicmVmcmVzaCJ8IG5fdmF1bHQKICBuX3ZhdWx0IC0uLT58ImxvZyByZWFkInwgbl9hdWRpdA%3FbgColor%3D0d1017%26type%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fYXBwKCJBcHAgc3RhcnRzPGJyLz5ObyBzZWNyZXRzIGJha2VkIGluIikKICBjbGFzcyBuX2FwcCBjb21wdXRlOwogIG5fYXV0aCgiQXV0aGVudGljYXRlPGJyLz5Xb3JrbG9hZCBpZGVudGl0eSAvIElBTSByb2xlIikKICBjbGFzcyBuX2F1dGggZWRnZTsKICBuX3ZhdWx0KCJTZWNyZXQgbWFuYWdlcjxici8-VmF1bHQgLyBBV1MgU00gLyBLTVMiKQogIGNsYXNzIG5fdmF1bHQgZGF0YTsKICBuX2luamVjdCgiSW5qZWN0IGF0IHJ1bnRpbWU8YnIvPkludG8gbWVtb3J5IC8gZW52IikKICBjbGFzcyBuX2luamVjdCBjb21wdXRlOwogIG5fdXNlKCJVc2Ugc2VjcmV0PGJyLz5DYWxsIERCIC8gQVBJIikKICBjbGFzcyBuX3VzZSBleHRlcm5hbDsKICBuX3JvdGF0ZSgiUm90YXRpb24gbG9vcDxici8-SXNzdWUgbmV3LCBleHBpcmUgb2xkIikKICBjbGFzcyBuX3JvdGF0ZSBvYnNlcnZhYmlsaXR5OwogIG5fYXVkaXQoIkF1ZGl0IGxvZzxici8-V2hvIHJlYWQgd2hhdCwgd2hlbiIpCiAgY2xhc3Mgbl9hdWRpdCBvYnNlcnZhYmlsaXR5OwogIG5fYXBwIC0tPnwiaWRlbnRpdHkifCBuX2F1dGgKICBuX2F1dGggLS0-fCJ2ZXJpZnkifCBuX3ZhdWx0CiAgbl92YXVsdCAtLT58InNlY3JldCJ8IG5faW5qZWN0CiAgbl9pbmplY3QgLS0-IG5fdXNlCiAgbl9yb3RhdGUgLS4tPnwicmVmcmVzaCJ8IG5fdmF1bHQKICBuX3ZhdWx0IC0uLT58ImxvZyByZWFkInwgbl9hdWRpdA%3FbgColor%3D0d1017%26type%3Dpng" alt="An app authenticates to the secret manager at startup, the secret is injected into memory at runtime, and a rotation loo" width="1279" height="230"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;An app authenticates to the secret manager at startup, the secret is injected into memory at runtime, and a rotation loop quietly replaces it on a schedule.&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;App boots with no secret&lt;/strong&gt;: The image and the repo contain zero credentials. There is nothing to leak even if the image is pulled or the repo is cloned.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;App proves who it is&lt;/strong&gt;: It authenticates to the secret manager using its workload identity, an IAM role, a Kubernetes service account, or a short-lived token, not a stored password.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manager checks the policy&lt;/strong&gt;: The secret manager confirms this identity is allowed to read this specific secret, then returns it over an encrypted channel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secret is injected at runtime&lt;/strong&gt;: The value lands in the process's memory (or a tmpfs-backed env var). It is never written to disk, the image, or version control.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotation runs in the background&lt;/strong&gt;: On a schedule, the manager issues a fresh value and expires the old one. Apps re-fetch and keep working, a leaked old copy becomes useless.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Where NOT to put secrets (and where to)
&lt;/h2&gt;

&lt;p&gt;Most leaks come from picking the wrong storage location, not from sophisticated attacks. Here is how the common options actually compare on the three things that matter: how exposed the secret is, whether you can rotate it cleanly, and whether you can see who used it.&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;Leak risk&lt;/th&gt;
&lt;th&gt;Rotation&lt;/th&gt;
&lt;th&gt;Audit trail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hardcoded in source&lt;/td&gt;
&lt;td&gt;Severe, lives forever in Git history, images, and every clone&lt;/td&gt;
&lt;td&gt;Painful, code change + redeploy everywhere&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.env committed to Git&lt;/td&gt;
&lt;td&gt;Severe, same as hardcoding, just in a different file&lt;/td&gt;
&lt;td&gt;Painful, and old commits still hold it&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Env var (set at deploy, not in Git)&lt;/td&gt;
&lt;td&gt;Moderate, can leak via logs, crash dumps, &lt;code&gt;/proc&lt;/code&gt;, child processes&lt;/td&gt;
&lt;td&gt;Manual, redeploy to change it&lt;/td&gt;
&lt;td&gt;Weak, no record of reads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secret manager (Vault / AWS SM / KMS)&lt;/td&gt;
&lt;td&gt;Low, encrypted at rest, scoped by policy, short-lived&lt;/td&gt;
&lt;td&gt;Automated, rotate on a schedule, no redeploy&lt;/td&gt;
&lt;td&gt;Full, every read is logged&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The further down this table you go, the smaller the blast radius when something goes wrong.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Warning:&lt;/strong&gt; A &lt;code&gt;.env&lt;/code&gt; file is fine for &lt;em&gt;local&lt;/em&gt; development as long as it is in &lt;code&gt;.gitignore&lt;/code&gt; and never committed. The danger is the moment it lands in version control, at that point it is identical to hardcoding the secret, and deleting it later does &lt;strong&gt;not&lt;/strong&gt; remove it from Git history.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Reading a secret the right way
&lt;/h2&gt;

&lt;p&gt;The code that consumes a secret should fetch it from the environment or a manager client, never embed the literal. The example below reads a database password from a secret manager with an env-var fallback for local dev, and fails loudly if it is missing rather than silently using a blank value.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;db.py&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;functools&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;lru_cache&lt;/span&gt;


&lt;span class="nd"&gt;@lru_cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxsize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_db_password&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Local dev: read from an env var that is NEVER committed.
&lt;/span&gt;    &lt;span class="n"&gt;local&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DB_PASSWORD&lt;/span&gt;&lt;span class="sh"&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;local&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;local&lt;/span&gt;

    &lt;span class="c1"&gt;# Production: fetch from AWS Secrets Manager at runtime.
&lt;/span&gt;    &lt;span class="n"&gt;secret_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DB_SECRET_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# just a name, not the value
&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;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;secretsmanager&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;resp&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="nf"&gt;get_secret_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SecretId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;secret_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;resp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SecretString&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_db_password&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c1"&gt;# ... open the connection; never log `password`
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;  &lt;span class="c1"&gt;# used in-memory only
&lt;/span&gt;

&lt;span class="c1"&gt;# NEVER do this:
# DB_PASSWORD = "hunter2-prod-9f3a"   # hardcoded, leaks on first commit
&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice three things: the &lt;strong&gt;value&lt;/strong&gt; of the secret never appears in the file (only the secret's &lt;em&gt;name&lt;/em&gt;, &lt;code&gt;DB_SECRET_ID&lt;/code&gt;), the result is cached so you do not hammer the manager on every call, and there is no &lt;code&gt;print(password)&lt;/code&gt; anywhere, secrets in logs are one of the most common silent leaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rotation and least-privilege
&lt;/h2&gt;

&lt;p&gt;Two practices turn "we have a secret manager" into "we are actually safe." &lt;strong&gt;Rotation&lt;/strong&gt; means every secret has an expiry; the manager issues a new value and retires the old one on a schedule, so a copy stolen six months ago is already dead. The gold standard is &lt;em&gt;dynamic secrets&lt;/em&gt;, credentials minted on demand that live for minutes, not months.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Least-privilege access&lt;/strong&gt; means each identity can read only the exact secrets it needs. The billing service has no business reading the email provider's API key. Scope policies per-service so a single compromised workload exposes one secret, not the whole vault. Pair this with the audit log: if you cannot answer "who read this secret and when," you cannot tell whether a leak has been used.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detecting leaked secrets
&lt;/h2&gt;

&lt;p&gt;Assume a secret will eventually slip through. Your job is to catch it fast. Run a secret scanner in pre-commit hooks &lt;strong&gt;and&lt;/strong&gt; in CI so a credential is blocked before it ever reaches the remote, and enable your Git host's push protection (GitHub and GitLab both scan pushes for known token formats).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;scan.sh&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="c"&gt;# Block secrets before they are committed (run as a pre-commit hook)&lt;/span&gt;
gitleaks protect &lt;span class="nt"&gt;--staged&lt;/span&gt; &lt;span class="nt"&gt;--verbose&lt;/span&gt;

&lt;span class="c"&gt;# Scan full history for anything already leaked&lt;/span&gt;
gitleaks detect &lt;span class="nt"&gt;--source&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--report-path&lt;/span&gt; leaks.json

&lt;span class="c"&gt;# If a real secret is found in history, ROTATE it first,&lt;/span&gt;
&lt;span class="c"&gt;# then purge from history (e.g. git filter-repo), deleting the&lt;/span&gt;
&lt;span class="c"&gt;# file in a new commit does NOT remove the old value.&lt;/span&gt;

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

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; When a secret leaks, &lt;strong&gt;rotate first, clean up second.&lt;/strong&gt; Revoking the credential makes the leaked copy worthless immediately; scrubbing Git history is housekeeping that can wait minutes. Doing it in the other order leaves a live key exposed while you wrestle with history rewrites.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Common mistakes that cost hours (or accounts)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Committing secrets to Git.&lt;/strong&gt; Even a deleted &lt;code&gt;.env&lt;/code&gt; lives forever in history. Once pushed, treat the secret as compromised and rotate it, do not just remove the file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never rotating.&lt;/strong&gt; A secret that has not changed in two years has likely been copied into someone's notes, a Slack message, or a screenshot. Set an expiry on everything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Granting broad access.&lt;/strong&gt; One "admin" policy that can read every secret turns any single compromised service into a full breach. Scope per-service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leaking secrets in logs.&lt;/strong&gt; &lt;code&gt;print(token)&lt;/code&gt;, verbose error traces, and crash dumps quietly ship credentials to your log aggregator, which often has far weaker access controls than your vault.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Putting secrets in container image layers or CI variables in plaintext.&lt;/strong&gt; Images get pushed to registries; CI logs get shared. Inject at runtime instead.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Secrets management in seven lines&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A secret is anything that lets someone act as you, keys, passwords, tokens, certs.&lt;/li&gt;
&lt;li&gt;Never hardcode secrets or commit &lt;code&gt;.env&lt;/code&gt; files; Git history is forever.&lt;/li&gt;
&lt;li&gt;Store secrets in a dedicated manager (Vault, AWS Secrets Manager, cloud KMS).&lt;/li&gt;
&lt;li&gt;Inject at runtime into memory using workload identity, not baked into images.&lt;/li&gt;
&lt;li&gt;Rotate on a schedule so a stolen copy expires; prefer short-lived dynamic secrets.&lt;/li&gt;
&lt;li&gt;Grant least-privilege: each service reads only the secrets it needs, and every read is logged.&lt;/li&gt;
&lt;li&gt;Scan for leaks in pre-commit and CI; if one leaks, rotate first, then clean history.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;Secrets management is one pillar of a secure delivery pipeline. Build the surrounding picture next:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Start with the fundamentals: &lt;a href="https://thesimplifiedtech.com/blog/what-is-application-security" rel="noopener noreferrer"&gt;What Is Application Security?&lt;/a&gt; frames where secrets fit among the broader threats.&lt;/li&gt;
&lt;li&gt;Then extend the discipline to your build inputs: &lt;a href="https://thesimplifiedtech.com/blog/securing-the-software-supply-chain" rel="noopener noreferrer"&gt;Securing the Software Supply Chain&lt;/a&gt; covers dependencies, provenance, and signing.&lt;/li&gt;
&lt;li&gt;Practice the delivery mechanics in the &lt;a href="https://thesimplifiedtech.com/labs/cicd" rel="noopener noreferrer"&gt;CI/CD lab&lt;/a&gt;, where injected secrets and pipeline variables come to life.&lt;/li&gt;
&lt;li&gt;See how this fits a full role on the &lt;a href="https://thesimplifiedtech.com/career-paths/devops-engineer" rel="noopener noreferrer"&gt;DevOps Engineer path&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://thesimplifiedtech.com/blog/secrets-management" rel="noopener noreferrer"&gt;TheSimplifiedTech&lt;/a&gt;, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>devsecops</category>
      <category>cybersecurity</category>
      <category>secrets</category>
    </item>
    <item>
      <title>The OWASP Top 10, Explained</title>
      <dc:creator>Sri Balaji</dc:creator>
      <pubDate>Sat, 19 Sep 2026 14:04:36 +0000</pubDate>
      <link>https://dev.to/sri2614/the-owasp-top-10-explained-48hp</link>
      <guid>https://dev.to/sri2614/the-owasp-top-10-explained-48hp</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚡ &lt;strong&gt;TL;DR:&lt;/strong&gt; A beginner-followable tour of the OWASP Top 10: what each web risk really is, a concrete example, and the specific fix. You will leave able to use the list as a working checklist, not just recite it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The list every attacker has already read&lt;/li&gt;
&lt;li&gt;One sentence, then a picture&lt;/li&gt;
&lt;li&gt;The OWASP Top 10 at a glance&lt;/li&gt;
&lt;li&gt;A few of these, made concrete&lt;/li&gt;
&lt;li&gt;How to use the Top 10 in practice&lt;/li&gt;
&lt;li&gt;Fixing a real one: a server-side ownership check&lt;/li&gt;
&lt;li&gt;Common mistakes that cost hours&lt;/li&gt;
&lt;li&gt;Takeaways&lt;/li&gt;
&lt;li&gt;Where to go next&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The list every attacker has already read
&lt;/h2&gt;

&lt;p&gt;You ship a feature. It works in the demo, the tests are green, the PR is approved. Three weeks later someone changes a number in a URL and reads another customer's invoices. Nothing crashed. No alarm fired. The code did exactly what you wrote, you just never told it who was allowed to ask.&lt;/p&gt;

&lt;p&gt;Almost every breach you read about traces back to a small, named, well-understood mistake. The &lt;strong&gt;OWASP Top 10&lt;/strong&gt; is the industry's shared list of those mistakes, the ten categories of web application risk that show up most often and hurt the most. It is maintained by the &lt;a href="https://owasp.org/Top10/" rel="noopener noreferrer"&gt;Open Worldwide Application Security Project&lt;/a&gt;, a nonprofit, and it is the single best starting map for anyone who builds or operates web software.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;Who this is for:&lt;/strong&gt; Developers, DevOps and platform engineers, and anyone shipping a web app or API who wants a working mental model of how things get broken, and a checklist they can act on this week. No security background assumed. If you can read a function, you can follow this.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We will walk all ten: what each one is, a concrete example, and how to prevent it. Then we will turn the list into a practical routine you can actually run.&lt;/p&gt;

&lt;h2&gt;
  
  
  One sentence, then a picture
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;The OWASP Top 10 is not a list of bugs, it is a list of the ten ways trust gets misplaced in a web application.&lt;/p&gt;

&lt;p&gt;The mental model worth keeping&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here is the analogy. Think of your application as a building. Most of the Top 10 are not exotic break-ins, they are the unlocked side door, the master key that opens every room, the contractor you let in without checking ID, and the security camera that was never plugged in.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;In the real world&lt;/th&gt;
&lt;th&gt;In tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;An unlocked side door anyone can walk through&lt;/td&gt;
&lt;td&gt;Broken Access Control, the server never checks who you are&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Valuables left on a desk by the window&lt;/td&gt;
&lt;td&gt;Cryptographic Failures, sensitive data stored or sent in the clear&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A contractor you let in without checking ID&lt;/td&gt;
&lt;td&gt;Injection, untrusted input treated as trusted instructions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A camera installed but never plugged in&lt;/td&gt;
&lt;td&gt;Logging &amp;amp; Monitoring Failures, the attack happened, nobody saw it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Each abstract risk maps to something physical you already understand.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Before the list, look at where these risks actually land. A web app is not one thing, it is a chain of trust boundaries, and each risk hits a specific link.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fY2xpZW50KCJCcm93c2VyIC8gQ2xpZW50PGJyLz5YU1MsIHdlYWsgYXV0aCBVWCIpCiAgY2xhc3Mgbl9jbGllbnQgY2xpZW50OwogIG5fYXBpKCJBUEkgLyBBcHAgU2VydmVyPGJyLz5BY2Nlc3MgY29udHJvbCwgaW5qZWN0aW9uLCBTU1JGIikKICBjbGFzcyBuX2FwaSBjb21wdXRlOwogIG5fZGIoIkRhdGFiYXNlPGJyLz5JbmplY3Rpb24sIGNyeXB0byBmYWlsdXJlcyIpCiAgY2xhc3Mgbl9kYiBkYXRhOwogIG5fZGVwcygiRGVwZW5kZW5jaWVzPGJyLz5WdWxuZXJhYmxlICYgb3V0ZGF0ZWQgY29tcG9uZW50cyIpCiAgY2xhc3Mgbl9kZXBzIGV4dGVybmFsOwogIG5fY29uZmlnKCJDb25maWcgLyBJbmZyYTxici8-U2VjdXJpdHkgbWlzY29uZmlndXJhdGlvbiIpCiAgY2xhc3Mgbl9jb25maWcgb2JzZXJ2YWJpbGl0eTsKICBuX2NsaWVudCAtLT58InJlcXVlc3QifCBuX2FwaQogIG5fYXBpIC0tPnwicXVlcnkifCBuX2RiCiAgbl9kZXBzIC0uLT58ImltcG9ydGVkInwgbl9hcGkKICBuX2NvbmZpZyAtLi0-fCJkZXBsb3llZCB3aXRoInwgbl9hcGk%3FbgColor%3D0d1017%26type%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fY2xpZW50KCJCcm93c2VyIC8gQ2xpZW50PGJyLz5YU1MsIHdlYWsgYXV0aCBVWCIpCiAgY2xhc3Mgbl9jbGllbnQgY2xpZW50OwogIG5fYXBpKCJBUEkgLyBBcHAgU2VydmVyPGJyLz5BY2Nlc3MgY29udHJvbCwgaW5qZWN0aW9uLCBTU1JGIikKICBjbGFzcyBuX2FwaSBjb21wdXRlOwogIG5fZGIoIkRhdGFiYXNlPGJyLz5JbmplY3Rpb24sIGNyeXB0byBmYWlsdXJlcyIpCiAgY2xhc3Mgbl9kYiBkYXRhOwogIG5fZGVwcygiRGVwZW5kZW5jaWVzPGJyLz5WdWxuZXJhYmxlICYgb3V0ZGF0ZWQgY29tcG9uZW50cyIpCiAgY2xhc3Mgbl9kZXBzIGV4dGVybmFsOwogIG5fY29uZmlnKCJDb25maWcgLyBJbmZyYTxici8-U2VjdXJpdHkgbWlzY29uZmlndXJhdGlvbiIpCiAgY2xhc3Mgbl9jb25maWcgb2JzZXJ2YWJpbGl0eTsKICBuX2NsaWVudCAtLT58InJlcXVlc3QifCBuX2FwaQogIG5fYXBpIC0tPnwicXVlcnkifCBuX2RiCiAgbl9kZXBzIC0uLT58ImltcG9ydGVkInwgbl9hcGkKICBuX2NvbmZpZyAtLi0-fCJkZXBsb3llZCB3aXRoInwgbl9hcGk%3FbgColor%3D0d1017%26type%3Dpng" alt="The attack surface of a typical web app, and which Top 10 risks strike at each layer." width="927" height="390"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The attack surface of a typical web app, and which Top 10 risks strike at each layer.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The happy path is the straight line: browser to API to database. The two dashed branches, your dependencies and your config, are the ones teams forget, and they are where two whole categories of the Top 10 live.&lt;/p&gt;

&lt;h2&gt;
  
  
  The OWASP Top 10 at a glance
&lt;/h2&gt;

&lt;p&gt;This is the centerpiece. Read it once top to bottom for the shape of the landscape, then keep it open as a reference. The order mirrors the 2021 list, which is ranked by real-world prevalence and impact.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Risk&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;How to prevent it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A01 Broken Access Control&lt;/td&gt;
&lt;td&gt;Users can act outside their permissions, read, edit, or delete data that isn't theirs by changing an ID, URL, or method.&lt;/td&gt;
&lt;td&gt;Deny by default. Enforce ownership and role checks server-side on every request, never in the UI alone.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A02 Cryptographic Failures&lt;/td&gt;
&lt;td&gt;Sensitive data (passwords, tokens, PII) is stored or transmitted weakly, or not encrypted at all.&lt;/td&gt;
&lt;td&gt;TLS everywhere; encrypt data at rest; hash passwords with bcrypt/argon2; never invent your own crypto.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A03 Injection&lt;/td&gt;
&lt;td&gt;Untrusted input is interpreted as code or commands, SQL, NoSQL, OS, LDAP, letting attackers rewrite your query.&lt;/td&gt;
&lt;td&gt;Use parameterized queries and ORMs; validate and escape input; never string-concatenate user data into commands.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A04 Insecure Design&lt;/td&gt;
&lt;td&gt;The flaw is in the plan, not the code, a missing security control the design never accounted for.&lt;/td&gt;
&lt;td&gt;Threat-model before building; add abuse cases to stories; use secure design patterns and rate limits by default.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A05 Security Misconfiguration&lt;/td&gt;
&lt;td&gt;Default passwords, verbose errors, open cloud buckets, unnecessary features left enabled.&lt;/td&gt;
&lt;td&gt;Harden every environment; disable defaults; automate config so dev, staging and prod match; minimize attack surface.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A06 Vulnerable &amp;amp; Outdated Components&lt;/td&gt;
&lt;td&gt;A dependency (library, framework, OS package) has a known CVE and you are still running it.&lt;/td&gt;
&lt;td&gt;Inventory dependencies; scan continuously; patch on a schedule; remove what you don't use.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A07 Identification &amp;amp; Auth Failures&lt;/td&gt;
&lt;td&gt;Weak login: brute-forceable passwords, broken session handling, missing MFA.&lt;/td&gt;
&lt;td&gt;Enforce strong passwords + MFA; rate-limit and lock out; use a vetted auth library; rotate and expire sessions.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A08 Software &amp;amp; Data Integrity Failures&lt;/td&gt;
&lt;td&gt;Code or data is trusted without verifying it wasn't tampered with, unsigned updates, poisoned CI/CD pipelines.&lt;/td&gt;
&lt;td&gt;Verify signatures and checksums; lock dependency versions; secure your build pipeline; never auto-load untrusted code.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A09 Logging &amp;amp; Monitoring Failures&lt;/td&gt;
&lt;td&gt;Attacks happen and nobody notices, no logs, no alerts, no way to investigate after the fact.&lt;/td&gt;
&lt;td&gt;Log security events with context; centralize and alert; test that alerts actually fire; keep an incident plan.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A10 Server-Side Request Forgery (SSRF)&lt;/td&gt;
&lt;td&gt;The server is tricked into making requests to internal systems the attacker can't reach directly.&lt;/td&gt;
&lt;td&gt;Validate and allowlist outbound URLs; block internal IP ranges; isolate egress; never fetch raw user-supplied URLs.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The OWASP Top 10, each risk, what it actually is, and the one move that prevents most of it.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; Notice that A01 (Broken Access Control) sits at the top. In OWASP's 2021 data it was the most common serious issue by a wide margin, more applications had it than any other category. If you fix one thing first, fix access control.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  A few of these, made concrete
&lt;/h2&gt;

&lt;p&gt;Tables are great for scanning, but risks click when you see them happen. Three quick ones.&lt;/p&gt;

&lt;h3&gt;
  
  
  Broken Access Control (A01)
&lt;/h3&gt;

&lt;p&gt;Your invoice page is at &lt;code&gt;/invoices/1043&lt;/code&gt;. A curious user changes it to &lt;code&gt;/invoices/1042&lt;/code&gt; and sees someone else's bill. The server happily returned it because the only check was "are you logged in?", never "is this yours?". This is called an &lt;strong&gt;insecure direct object reference (IDOR)&lt;/strong&gt;, and it is the most common breach you will ever cause by accident.&lt;/p&gt;

&lt;h3&gt;
  
  
  Injection (A03)
&lt;/h3&gt;

&lt;p&gt;A login form builds a query like &lt;code&gt;SELECT * FROM users WHERE name = '" + input + "'&lt;/code&gt;. The attacker types &lt;code&gt;' OR '1'='1&lt;/code&gt; as the username, and suddenly the WHERE clause is always true. They are in. The fix is mechanical: stop building queries by gluing strings together. We cover this in depth in &lt;a href="https://thesimplifiedtech.com/blog/preventing-injection-attacks-sqli-xss" rel="noopener noreferrer"&gt;Preventing Injection Attacks (SQLi &amp;amp; XSS)&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Server-Side Request Forgery (A10)
&lt;/h3&gt;

&lt;p&gt;Your app has an "import image from URL" feature. An attacker submits &lt;code&gt;http://169.254.169.254/latest/meta-data/&lt;/code&gt;, the cloud metadata endpoint, and your server, which can reach it, fetches and returns your cloud credentials. The user could never reach that address; your server could, and you let it become a proxy.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Warning:&lt;/strong&gt; The pattern across all three: the server trusted input it should have questioned. Almost the entire Top 10 is variations on that one sentence.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  How to use the Top 10 in practice
&lt;/h2&gt;

&lt;p&gt;A list you read once and forget changes nothing. Here is how to turn it into a routine that actually catches issues, on a feature, a service, or a whole codebase.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Map your attack surface&lt;/strong&gt;: Sketch the layers from the diagram above for your own app: client, API, database, dependencies, config. You can't defend trust boundaries you haven't drawn.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Walk the data flow&lt;/strong&gt;: Follow one real request end to end. At every hop ask: where does untrusted input enter, and where do I trust it without checking? Those points are your candidates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Score each risk against your app&lt;/strong&gt;: Go down the Top 10 row by row and ask "could this happen here?" Mark each Yes / No / Not sure. "Not sure" is the answer that needs follow-up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix highest-impact first&lt;/strong&gt;: Start with A01 access control and A03 injection, they are common and devastating. Don't polish logging while the side door is unlocked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automate the repeatable parts&lt;/strong&gt;: Add a dependency scanner (A06) and secret/config checks (A05) to CI so regressions get caught without a human remembering to look.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-run it on every meaningful change&lt;/strong&gt;: New endpoint, new dependency, new integration? Re-walk the relevant rows. Security is a habit, not a one-time audit.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Fixing a real one: a server-side ownership check
&lt;/h2&gt;

&lt;p&gt;Let's fix the A01 example from earlier. The vulnerable version checks that you are logged in, then trusts the ID in the URL. The fixed version checks that the record actually belongs to you, on the server, where the user can't tamper with it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;invoices.py&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# VULNERABLE: any logged-in user can read any invoice
&lt;/span&gt;&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/invoices/&amp;lt;invoice_id&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@login_required&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_invoice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;invoice&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="n"&gt;invoices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_by_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# never checked who owns it
&lt;/span&gt;

&lt;span class="c1"&gt;# FIXED: deny by default, enforce ownership server-side
&lt;/span&gt;&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/invoices/&amp;lt;invoice_id&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@login_required&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_invoice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;invoice&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="n"&gt;invoices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_by_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 1. Does it exist at all?
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;invoice&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 2. Does it belong to the caller? (the check that matters)
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;owner_id&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# 404, not 403, don't even confirm the record exists
&lt;/span&gt;        &lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things to notice. First, the ownership check uses &lt;code&gt;current_user.id&lt;/code&gt; from the verified session, never an ID sent by the client. Second, when the record isn't yours we return &lt;strong&gt;404, not 403&lt;/strong&gt;: a 403 quietly confirms the invoice exists, which leaks information. Deny by default, and reveal nothing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; Best practice is to centralize this. Instead of repeating the ownership check in every handler, wrap it in a policy/authorization layer (e.g. a decorator or middleware) so a new endpoint is secure by default and a forgotten check is impossible rather than easy.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Common mistakes that cost hours
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Checking permissions in the UI only.&lt;/strong&gt; Hiding a button is not security, the API behind it must enforce the rule. Attackers call the API directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trusting an ID from the request to decide ownership.&lt;/strong&gt; If the client sends &lt;code&gt;user_id=42&lt;/code&gt;, an attacker sends &lt;code&gt;user_id=7&lt;/code&gt;. Derive identity from the verified session, never the payload.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating the Top 10 as a one-time audit.&lt;/strong&gt; It is a recurring lens. A new endpoint added next week reintroduces A01 if nobody re-checks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rolling your own crypto or auth.&lt;/strong&gt; Hashing passwords with SHA-256, hand-writing JWT validation, inventing a token scheme, use vetted libraries (bcrypt/argon2, a maintained auth framework) instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting dependencies and config (A05, A06).&lt;/strong&gt; Teams obsess over their own code and ship a 3-year-old library with a known CVE or a publicly readable storage bucket.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logging the wrong things.&lt;/strong&gt; Either nothing useful (so you're blind during an incident) or too much, dumping passwords and tokens into logs, which becomes a breach of its own.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The whole article in seven lines&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The OWASP Top 10 is the shared map of the ten ways web apps most often get broken.&lt;/li&gt;
&lt;li&gt;Almost every entry reduces to one idea: the server trusted input it should have questioned.&lt;/li&gt;
&lt;li&gt;Broken Access Control (A01) is the most common serious risk, fix it first.&lt;/li&gt;
&lt;li&gt;Enforce authorization server-side, on every request, deny-by-default, using verified identity.&lt;/li&gt;
&lt;li&gt;Stop concatenating user input into queries or commands, parameterize everything (A03).&lt;/li&gt;
&lt;li&gt;Don't forget the dashed branches: dependencies (A06) and configuration (A05).&lt;/li&gt;
&lt;li&gt;If you can't see an attack, you can't stop it, logging and monitoring (A09) is not optional.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;The Top 10 is the map; the next step is going deep on the territory. Start with the categories you marked "Yes" or "Not sure" in the practice walkthrough.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/preventing-injection-attacks-sqli-xss" rel="noopener noreferrer"&gt;Preventing Injection Attacks (SQLi &amp;amp; XSS)&lt;/a&gt;, A03 in depth, with the exact query patterns that are safe and the ones that aren't.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/secure-api-design" rel="noopener noreferrer"&gt;Secure API Design&lt;/a&gt;, how to build access control, input validation, and rate limiting in from the start rather than bolting them on.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/authentication-vs-authorization" rel="noopener noreferrer"&gt;Authentication vs Authorization&lt;/a&gt;, the distinction underneath A01 and A07: who you are versus what you're allowed to do.&lt;/li&gt;
&lt;li&gt;Practice the surrounding skills on the &lt;a href="https://thesimplifiedtech.com/career-paths/devops-engineer" rel="noopener noreferrer"&gt;DevOps Engineer path&lt;/a&gt;, where security threads through pipelines, config, and infrastructure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Read the &lt;a href="https://owasp.org/Top10/" rel="noopener noreferrer"&gt;official OWASP Top 10&lt;/a&gt; once for the canonical detail, but you now have the mental model to make it land. Pick one risk, run the six-step routine against something you own this week, and fix what you find.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://thesimplifiedtech.com/blog/the-owasp-top-10-explained" rel="noopener noreferrer"&gt;TheSimplifiedTech&lt;/a&gt;, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>devsecops</category>
      <category>cybersecurity</category>
      <category>owasp</category>
    </item>
    <item>
      <title>Authentication vs Authorization: The Two Most-Confused Words in Security</title>
      <dc:creator>Sri Balaji</dc:creator>
      <pubDate>Fri, 18 Sep 2026 14:43:04 +0000</pubDate>
      <link>https://dev.to/sri2614/authentication-vs-authorization-the-two-most-confused-words-in-security-1l03</link>
      <guid>https://dev.to/sri2614/authentication-vs-authorization-the-two-most-confused-words-in-security-1l03</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚡ &lt;strong&gt;TL;DR:&lt;/strong&gt; Authentication proves who you are; authorization decides what you can do. Run them in that order, carry identity with sessions or tokens, and put each check in the right place. Confuse the two and you ship security holes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Same prefix, completely different jobs&lt;/li&gt;
&lt;li&gt;Two questions, in order&lt;/li&gt;
&lt;li&gt;Where each check lives in a request&lt;/li&gt;
&lt;li&gt;Sessions vs tokens: how identity is carried&lt;/li&gt;
&lt;li&gt;OAuth and OIDC, without the jargon&lt;/li&gt;
&lt;li&gt;In code: verify identity, then check permission&lt;/li&gt;
&lt;li&gt;Common mistakes that ship security holes&lt;/li&gt;
&lt;li&gt;Takeaways&lt;/li&gt;
&lt;li&gt;Where to go next&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Same prefix, completely different jobs
&lt;/h2&gt;

&lt;p&gt;You add a login page, users sign in, and you breathe out. The app feels secure. Then a junior engineer signs in with their own valid account, changes the URL from &lt;code&gt;/orders/42&lt;/code&gt; to &lt;code&gt;/orders/43&lt;/code&gt;, and sees someone else's invoice. Nothing was "hacked", every request had a valid session. The login worked perfectly. The problem was never login at all.&lt;/p&gt;

&lt;p&gt;That gap is the difference between &lt;strong&gt;authentication&lt;/strong&gt; and &lt;strong&gt;authorization&lt;/strong&gt;. They share a prefix, they sit next to each other in the request, and they get used interchangeably in standups, which is exactly why people ship the bug above. Once you can keep them apart in your head, a huge class of security mistakes simply stops happening.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;Who this is for:&lt;/strong&gt; Developers who can build a login form but get fuzzy on what happens after sign-in. If you have ever wondered "is this a 401 or a 403?", written an &lt;code&gt;if (user.isAdmin)&lt;/code&gt; check in a React component, or copy-pasted JWT verification without quite knowing what it proves, this is for you. No prior security background needed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Two questions, in order
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Authentication (authn) answers "who are you?", it proves identity. Authorization (authz) answers "what are you allowed to do?", it grants or denies access to a specific action or resource. Authn always runs first; authz depends on its answer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Notice the ordering. You cannot decide what someone is allowed to do until you know who they are. But knowing who they are tells you nothing, on its own, about what they may do. Those are two separate decisions, made at two separate moments, often by two separate parts of your system.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;In the real world&lt;/th&gt;
&lt;th&gt;In tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Showing your passport at the security checkpoint&lt;/td&gt;
&lt;td&gt;Authentication, the system confirms you are who you claim to be&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Your boarding pass says seat 14C on flight BA42 today&lt;/td&gt;
&lt;td&gt;Authorization, you may board THIS flight, in THIS seat, right now&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A valid passport does not let you board any plane you want&lt;/td&gt;
&lt;td&gt;Being authenticated does not mean you are authorized for an action&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The lounge agent re-checks your pass at the gate&lt;/td&gt;
&lt;td&gt;Authorization is re-checked at each protected resource, not just once&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The airport makes the split obvious, two checkpoints, two different questions.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Your passport is real whether you are flying to Tokyo or standing in a parking lot, identity is stable. The boarding pass is scoped: one flight, one seat, one day. That scoping is the whole point of authorization, and it is why "they logged in" is never a complete answer to "should they be able to do this?".&lt;/p&gt;

&lt;h2&gt;
  
  
  Where each check lives in a request
&lt;/h2&gt;

&lt;p&gt;Every protected request runs the same gauntlet. The client presents a credential, the server verifies it to establish identity, then a separate step asks whether that identity may perform this exact action on this exact resource. Only if both pass does your business logic ever run.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fY2xpZW50KCJDbGllbnQ8YnIvPnNlbmRzIGNyZWRlbnRpYWwiKQogIGNsYXNzIG5fY2xpZW50IGNsaWVudDsKICBuX2F1dGhuKCJBdXRoZW50aWNhdGU8YnIvPldobyBhcmUgeW91PyIpCiAgY2xhc3Mgbl9hdXRobiBlZGdlOwogIG5fYXV0aHooIkF1dGhvcml6ZTxici8-QWxsb3dlZCBmb3IgdGhpcz8iKQogIGNsYXNzIG5fYXV0aHogY29tcHV0ZTsKICBuX2hhbmRsZXIoIkhhbmRsZXI8YnIvPkJ1c2luZXNzIGxvZ2ljIikKICBjbGFzcyBuX2hhbmRsZXIgY29tcHV0ZTsKICBuX3N0b3JlKCJJZGVudGl0eSAvIFBvbGljeTxici8-dXNlcnMsIHJvbGVzLCBzY29wZXMiKQogIGNsYXNzIG5fc3RvcmUgZGF0YTsKICBuX3JlamVjdCgiNDAxIC8gNDAzPGJyLz5yZWplY3RlZCBlYXJseSIpCiAgY2xhc3Mgbl9yZWplY3QgZXh0ZXJuYWw7CiAgbl9jbGllbnQgLS0-fCJ0b2tlbiAvIGNvb2tpZSJ8IG5fYXV0aG4KICBuX2F1dGhuIC0tPnwiaWRlbnRpdHkifCBuX2F1dGh6CiAgbl9hdXRoeiAtLT58InBlcm1pdHRlZCJ8IG5faGFuZGxlcgogIG5fc3RvcmUgLS4tPiBuX2F1dGhuCiAgbl9zdG9yZSAtLi0-IG5fYXV0aHoKICBuX2F1dGhuIC0uLT58Im5vL2ludmFsaWQgY3JlZHMgKDQwMSkifCBuX3JlamVjdAogIG5fYXV0aHogLS4tPnwibm90IGFsbG93ZWQgKDQwMykifCBuX3JlamVjdA%3FbgColor%3D0d1017%26type%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fY2xpZW50KCJDbGllbnQ8YnIvPnNlbmRzIGNyZWRlbnRpYWwiKQogIGNsYXNzIG5fY2xpZW50IGNsaWVudDsKICBuX2F1dGhuKCJBdXRoZW50aWNhdGU8YnIvPldobyBhcmUgeW91PyIpCiAgY2xhc3Mgbl9hdXRobiBlZGdlOwogIG5fYXV0aHooIkF1dGhvcml6ZTxici8-QWxsb3dlZCBmb3IgdGhpcz8iKQogIGNsYXNzIG5fYXV0aHogY29tcHV0ZTsKICBuX2hhbmRsZXIoIkhhbmRsZXI8YnIvPkJ1c2luZXNzIGxvZ2ljIikKICBjbGFzcyBuX2hhbmRsZXIgY29tcHV0ZTsKICBuX3N0b3JlKCJJZGVudGl0eSAvIFBvbGljeTxici8-dXNlcnMsIHJvbGVzLCBzY29wZXMiKQogIGNsYXNzIG5fc3RvcmUgZGF0YTsKICBuX3JlamVjdCgiNDAxIC8gNDAzPGJyLz5yZWplY3RlZCBlYXJseSIpCiAgY2xhc3Mgbl9yZWplY3QgZXh0ZXJuYWw7CiAgbl9jbGllbnQgLS0-fCJ0b2tlbiAvIGNvb2tpZSJ8IG5fYXV0aG4KICBuX2F1dGhuIC0tPnwiaWRlbnRpdHkifCBuX2F1dGh6CiAgbl9hdXRoeiAtLT58InBlcm1pdHRlZCJ8IG5faGFuZGxlcgogIG5fc3RvcmUgLS4tPiBuX2F1dGhuCiAgbl9zdG9yZSAtLi0-IG5fYXV0aHoKICBuX2F1dGhuIC0uLT58Im5vL2ludmFsaWQgY3JlZHMgKDQwMSkifCBuX3JlamVjdAogIG5fYXV0aHogLS4tPnwibm90IGFsbG93ZWQgKDQwMykifCBuX3JlamVjdA%3FbgColor%3D0d1017%26type%3Dpng" alt="One request, two gates: authenticate (who), then authorize (allowed for this action?), then the handler runs." width="1007" height="230"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One request, two gates: authenticate (who), then authorize (allowed for this action?), then the handler runs.&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Client sends a credential&lt;/strong&gt;: A request arrives carrying proof of identity, a session cookie, a bearer JWT in the Authorization header, or an API key. No credential at all means the request is anonymous.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authenticate: who are you?&lt;/strong&gt;: The server validates the credential, verifies the JWT signature and expiry, or looks up the session ID in its store. Success yields a trusted identity (a user id, maybe roles/scopes). Failure returns 401 Unauthorized and stops here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorize: allowed for THIS?&lt;/strong&gt;: Now that identity is known, a policy check asks whether this user may perform this specific action on this specific resource, not "are they logged in" but "may user 42 delete order 43?". Failure returns 403 Forbidden.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handler runs&lt;/strong&gt;: Only after both gates pass does the actual business logic execute. By the time your handler runs, identity is proven and the action is permitted, the handler should not have to re-litigate either.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; The two failure codes map cleanly: &lt;strong&gt;401 Unauthorized&lt;/strong&gt; means "I don't know who you are" (authn failed, go log in). &lt;strong&gt;403 Forbidden&lt;/strong&gt; means "I know exactly who you are, and you still can't do this" (authz failed, logging in again won't help). Despite its name, 401 is the authentication error.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Sessions vs tokens: how identity is carried
&lt;/h2&gt;

&lt;p&gt;Authentication does not stop after login, proof of identity has to ride along on every subsequent request, because HTTP is stateless. There are two dominant ways to carry it, and they make opposite trade-offs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sessions&lt;/strong&gt; are server-side. After login the server creates a session record (in memory, Redis, or a database) and hands the client an opaque session ID in a cookie. Every request sends the cookie; the server looks the ID up to recover the identity. The token itself carries no information, it is just a key into server state. Revoking access is trivial: delete the row.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tokens&lt;/strong&gt; (typically JWTs) are self-contained. After login the server signs a token that embeds the claims, user id, roles, expiry, and the client sends it on each request. The server verifies the signature and trusts the contents without a lookup, which scales beautifully across many services. The catch: a signed token is valid until it expires, so you cannot easily revoke one mid-flight. That is why access tokens should be short-lived and paired with a refresh token.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Server sessions&lt;/th&gt;
&lt;th&gt;JWT / tokens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Where state lives&lt;/td&gt;
&lt;td&gt;Server (store / DB)&lt;/td&gt;
&lt;td&gt;Inside the token itself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lookup per request&lt;/td&gt;
&lt;td&gt;Yes, fetch the session&lt;/td&gt;
&lt;td&gt;No, verify the signature&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Revocation&lt;/td&gt;
&lt;td&gt;Instant (delete the record)&lt;/td&gt;
&lt;td&gt;Hard until expiry; needs a denylist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scales across services&lt;/td&gt;
&lt;td&gt;Needs shared session store&lt;/td&gt;
&lt;td&gt;Naturally, any service can verify&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best lifetime&lt;/td&gt;
&lt;td&gt;Long-ish, revocable&lt;/td&gt;
&lt;td&gt;Short access + refresh token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Carried in&lt;/td&gt;
&lt;td&gt;Cookie (HttpOnly)&lt;/td&gt;
&lt;td&gt;Authorization: Bearer header&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Sessions vs tokens, the trade-off is statefulness vs revocability.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  OAuth and OIDC, without the jargon
&lt;/h2&gt;

&lt;p&gt;Two more terms get tangled into the authn/authz knot, so let us untangle them in one breath. &lt;strong&gt;OAuth 2.0 is an authorization framework&lt;/strong&gt;, it lets a user grant one app limited access to their data in another app without sharing a password. When you click "Connect your Google Calendar", OAuth is what issues an access token scoped to just your calendar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OIDC (OpenID Connect) is an authentication layer built on top of OAuth.&lt;/strong&gt; It adds an &lt;code&gt;id_token&lt;/code&gt;, a JWT that proves who the user is, so apps can use "Sign in with Google" for login, not just data access. Rule of thumb: OAuth = delegated authorization (access to resources); OIDC = federated authentication (proof of identity). Most "Sign in with X" buttons are OIDC; most "Allow X to access your Y" consent screens are OAuth scopes.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;scopes&lt;/strong&gt; you see on those consent screens (&lt;code&gt;read:calendar&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;profile&lt;/code&gt;) are authorization data riding inside an authentication flow. Your app should treat scopes as one input to its own policy decisions, not as the final word on what a user may do inside your system.&lt;/p&gt;

&lt;h2&gt;
  
  
  In code: verify identity, then check permission
&lt;/h2&gt;

&lt;p&gt;Here is the whole idea in one Python handler. The first block proves identity from a JWT (authentication). The second block, a completely separate decision, asks whether that proven identity may act on this particular resource (authorization). Keep them visibly distinct.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;orders_api.py&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;  &lt;span class="c1"&gt;# PyJWT
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;

&lt;span class="n"&gt;JWT_SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...load-from-env...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;# --- AUTHENTICATION: who are you? ---
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&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;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# No credential at all -&amp;gt; we don't know who you are.
&lt;/span&gt;        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Missing token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeprefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Verifies signature AND expiry. Returns the claims if valid.
&lt;/span&gt;        &lt;span class="n"&gt;claims&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;algorithms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HS256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InvalidTokenError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invalid token&lt;/span&gt;&lt;span class="sh"&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;claims&lt;/span&gt;  &lt;span class="c1"&gt;# e.g. {"sub": "42", "roles": ["customer"]}
&lt;/span&gt;

&lt;span class="c1"&gt;# --- AUTHORIZATION: are you allowed to do THIS? ---
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;authorize_order_access&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="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;is_owner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sub&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;is_admin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;admin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;roles&lt;/span&gt;&lt;span class="sh"&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="ow"&gt;not&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;is_owner&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;is_admin&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# We know exactly who you are -- you still can't have this.
&lt;/span&gt;        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Forbidden&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;              &lt;span class="c1"&gt;# 1. prove identity
&lt;/span&gt;    &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&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;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_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;order&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;authorize_order_access&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;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;# 2. check permission
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;                               &lt;span class="c1"&gt;# 3. only now: business logic
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The authorization check is &lt;strong&gt;resource-scoped&lt;/strong&gt;: it compares the resource's owner against the caller. That is what stops the &lt;code&gt;/orders/43&lt;/code&gt; attack from the intro, a valid token authenticates the attacker, but they are not the owner, so &lt;code&gt;authorize_order_access&lt;/code&gt; returns 403. The bug at the top of this article is exactly an app that ran step 1 and skipped step 2.&lt;/p&gt;

&lt;p&gt;This pattern of "is the caller the owner, or do they have a role that overrides it?" is &lt;strong&gt;RBAC&lt;/strong&gt; (role-based access control). When rules get richer, "managers can refund orders under €500 in their own region", you graduate to &lt;strong&gt;ABAC&lt;/strong&gt; (attribute-based access control), where the decision is a function of attributes of the user, the resource, and the context. Same place in the request; just a more expressive policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes that ship security holes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Assuming authentication implies authorization.&lt;/strong&gt; "They're logged in" is not "they may do this." Every protected action needs its own permission check against the specific resource, the single most common real-world breach (broken object-level authorization, the &lt;code&gt;/orders/43&lt;/code&gt; bug).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enforcing authorization only in the UI.&lt;/strong&gt; Hiding the Delete button for non-admins is UX, not security. The endpoint is still reachable with curl. Every check that hides a button in the frontend must be re-enforced on the server, where it cannot be bypassed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long-lived or non-revocable tokens.&lt;/strong&gt; A JWT with a 30-day expiry and no denylist means a leaked token is a 30-day master key. Keep access tokens short (minutes), use refresh tokens, and have a revocation path for the cases that matter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trusting claims you didn't verify.&lt;/strong&gt; Decoding a JWT is not verifying it. Always check the signature and expiry (and issuer/audience) before trusting a single claim, an unverified &lt;code&gt;"roles": ["admin"]&lt;/code&gt; is just attacker-supplied JSON.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checking permissions far from the resource.&lt;/strong&gt; Authorization decided at the gateway with stale role data can drift from reality. Decide as close to the protected resource as you can, with current data.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The whole article in seven lines&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Authentication = who are you. Authorization = what may you do. Authn runs first; authz depends on it.&lt;/li&gt;
&lt;li&gt;401 = I don't know you (authn failed). 403 = I know you and you still can't (authz failed).&lt;/li&gt;
&lt;li&gt;Sessions store state on the server (easy revoke); tokens carry state in the JWT (easy scale, hard revoke).&lt;/li&gt;
&lt;li&gt;OAuth = delegated authorization (resource access). OIDC = authentication on top of OAuth (proof of identity).&lt;/li&gt;
&lt;li&gt;Authorization must be resource-scoped: not "are you logged in" but "may YOU touch THIS".&lt;/li&gt;
&lt;li&gt;Always enforce authorization on the server, UI hiding is not security.&lt;/li&gt;
&lt;li&gt;Short-lived tokens, verify signatures, decide permissions close to the resource.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;Authn and authz are the spine of every secure system, but they live inside a bigger picture. Two natural next steps: see how these checks fit into a hardened API surface, and how identity stops being a one-time gate and becomes a continuous decision.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/secure-api-design" rel="noopener noreferrer"&gt;Secure API Design&lt;/a&gt;, where authn/authz sit among input validation, rate limiting, and safe defaults.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/zero-trust-networking-beginners" rel="noopener noreferrer"&gt;Zero Trust Networking for Beginners&lt;/a&gt;, "never trust, always verify": authorization re-checked on every hop, not just at the edge.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/career-paths/devops-engineer" rel="noopener noreferrer"&gt;DevOps Engineer path&lt;/a&gt;, the role track where securing services and pipelines is part of the day job.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://thesimplifiedtech.com/blog/authentication-vs-authorization" rel="noopener noreferrer"&gt;TheSimplifiedTech&lt;/a&gt;, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>devsecops</category>
      <category>cybersecurity</category>
      <category>authentication</category>
    </item>
    <item>
      <title>What Is Application Security?</title>
      <dc:creator>Sri Balaji</dc:creator>
      <pubDate>Thu, 17 Sep 2026 15:22:38 +0000</pubDate>
      <link>https://dev.to/sri2614/what-is-application-security-p5g</link>
      <guid>https://dev.to/sri2614/what-is-application-security-p5g</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚡ &lt;strong&gt;TL;DR:&lt;/strong&gt; AppSec in one read: protect confidentiality, integrity, and availability by mapping trust boundaries and attack surface, then layering defense in depth and least privilege. Security is a phase of the whole SDLC, not a final gate.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Your app works. Now someone is trying to break it.&lt;/li&gt;
&lt;li&gt;A one-sentence definition&lt;/li&gt;
&lt;li&gt;Where security lives in the SDLC (shift-left)&lt;/li&gt;
&lt;li&gt;Trust boundaries and attack surface&lt;/li&gt;
&lt;li&gt;The principles that hold it all together&lt;/li&gt;
&lt;li&gt;The main domains of AppSec&lt;/li&gt;
&lt;li&gt;Common misconceptions that get people breached&lt;/li&gt;
&lt;li&gt;Takeaways&lt;/li&gt;
&lt;li&gt;Where to go next&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Your app works. Now someone is trying to break it.
&lt;/h2&gt;

&lt;p&gt;You ship a feature. It works in the demo, the tests are green, and users are happy. Then one quiet afternoon a stranger pastes a weird string into your login box, and suddenly they are reading other people's data, or your database is gone. Nothing "broke" in the way a bug breaks. The code did exactly what it was told. It was just told the wrong thing by the wrong person.&lt;/p&gt;

&lt;p&gt;That gap, between &lt;em&gt;what your app does&lt;/em&gt; and &lt;em&gt;what an attacker can convince it to do&lt;/em&gt;, is the whole subject of &lt;strong&gt;application security&lt;/strong&gt; (AppSec). It is not a product you buy at the end. It is a way of thinking that runs through every line you write, every dependency you pull, and every server you deploy to.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;Who this is for:&lt;/strong&gt; Developers, students, and new engineers who can build a working app but have never been taught to think about &lt;em&gt;who is attacking it&lt;/em&gt;. No prior security background needed. By the end you will have a mental model and the core vocabulary the rest of this track builds on.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  A one-sentence definition
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Application security is the practice of building, deploying, and running software so that it keeps doing the right thing even when someone is actively trying to make it do the wrong thing.&lt;/p&gt;

&lt;p&gt;The working definition we'll use all track&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Read that twice. The key phrase is &lt;em&gt;even when someone is actively trying&lt;/em&gt;. Ordinary engineering asks "does it work for a cooperative user?" Security asks "what happens when the user is hostile, the input is malicious, and the network is untrusted?" Same code, much harder question.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;In the real world&lt;/th&gt;
&lt;th&gt;In tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A fence around the property&lt;/td&gt;
&lt;td&gt;A firewall / network perimeter, keeps out casual passers-by, not a determined intruder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Locks on every door, not just the front&lt;/td&gt;
&lt;td&gt;Auth checks on every endpoint, not only the login page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Different keys for different rooms&lt;/td&gt;
&lt;td&gt;Least privilege, each component gets only the access it needs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security guards who verify ID&lt;/td&gt;
&lt;td&gt;Authentication and authorization at each trust boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cameras and an alarm log&lt;/td&gt;
&lt;td&gt;Logging, monitoring, and alerting, you detect the break-in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A safe inside the locked office&lt;/td&gt;
&lt;td&gt;Encryption, even past the doors, the valuables stay sealed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Securing software is like securing a building, no single measure is enough.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Notice what the analogy rules out: a fence alone is not security. Beginners often equate "we have a firewall" or "we use HTTPS" with "we are secure." Real security is &lt;em&gt;layers&lt;/em&gt;, many independent measures, so that one failure does not hand over the building. That idea has a name, and we will get to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where security lives in the SDLC (shift-left)
&lt;/h2&gt;

&lt;p&gt;The most expensive myth in software is that security is a step near the end, a scan you run, or a pen-test you book, the week before launch. By then the architecture is set, the risky shortcuts are buried, and every fix is a rewrite. The modern approach is &lt;strong&gt;shift-left&lt;/strong&gt;: push security earlier, into every phase of the software development lifecycle, so each phase has its own gate.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fZGVzaWduKCJEZXNpZ248YnIvPlRocmVhdCBtb2RlbGluZyIpCiAgY2xhc3Mgbl9kZXNpZ24gY2xpZW50OwogIG5fY29kZSgiQ29kZTxici8-U2VjdXJlIGNvZGluZyArIFNBU1QiKQogIGNsYXNzIG5fY29kZSBjb21wdXRlOwogIG5fYnVpbGQoIkJ1aWxkPGJyLz5EZXBlbmRlbmN5IC8gU0NBIHNjYW4iKQogIGNsYXNzIG5fYnVpbGQgY29tcHV0ZTsKICBuX2RlcGxveSgiRGVwbG95PGJyLz5Db25maWcgKyBzZWNyZXRzIGNoZWNrIikKICBjbGFzcyBuX2RlcGxveSBlZGdlOwogIG5fcnVuKCJSdW48YnIvPk1vbml0b3IgKyBwYXRjaCIpCiAgY2xhc3Mgbl9ydW4gb2JzZXJ2YWJpbGl0eTsKICBuX3RocmVhdHMoIkF0dGFja2Vyczxici8-UHJvYmluZyBjb25zdGFudGx5IikKICBjbGFzcyBuX3RocmVhdHMgZXh0ZXJuYWw7CiAgbl9mZWVkYmFjaygiRmluZGluZ3M8YnIvPkJ1Z3MsIGFsZXJ0cywgaW5jaWRlbnRzIikKICBjbGFzcyBuX2ZlZWRiYWNrIGRhdGE7CiAgbl9kZXNpZ24gLS0-fCJyZXZpZXcifCBuX2NvZGUKICBuX2NvZGUgLS0-fCJtZXJnZSJ8IG5fYnVpbGQKICBuX2J1aWxkIC0tPnwicHJvbW90ZSJ8IG5fZGVwbG95CiAgbl9kZXBsb3kgLS0-fCJyZWxlYXNlInwgbl9ydW4KICBuX3RocmVhdHMgLS4tPnwiYXR0YWNrInwgbl9ydW4KICBuX3J1biAtLi0-fCJkZXRlY3QifCBuX2ZlZWRiYWNrCiAgbl9mZWVkYmFjayAtLi0-fCJmZWVkIGJhY2sifCBuX2Rlc2lnbg%3FbgColor%3D0d1017%26type%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fZGVzaWduKCJEZXNpZ248YnIvPlRocmVhdCBtb2RlbGluZyIpCiAgY2xhc3Mgbl9kZXNpZ24gY2xpZW50OwogIG5fY29kZSgiQ29kZTxici8-U2VjdXJlIGNvZGluZyArIFNBU1QiKQogIGNsYXNzIG5fY29kZSBjb21wdXRlOwogIG5fYnVpbGQoIkJ1aWxkPGJyLz5EZXBlbmRlbmN5IC8gU0NBIHNjYW4iKQogIGNsYXNzIG5fYnVpbGQgY29tcHV0ZTsKICBuX2RlcGxveSgiRGVwbG95PGJyLz5Db25maWcgKyBzZWNyZXRzIGNoZWNrIikKICBjbGFzcyBuX2RlcGxveSBlZGdlOwogIG5fcnVuKCJSdW48YnIvPk1vbml0b3IgKyBwYXRjaCIpCiAgY2xhc3Mgbl9ydW4gb2JzZXJ2YWJpbGl0eTsKICBuX3RocmVhdHMoIkF0dGFja2Vyczxici8-UHJvYmluZyBjb25zdGFudGx5IikKICBjbGFzcyBuX3RocmVhdHMgZXh0ZXJuYWw7CiAgbl9mZWVkYmFjaygiRmluZGluZ3M8YnIvPkJ1Z3MsIGFsZXJ0cywgaW5jaWRlbnRzIikKICBjbGFzcyBuX2ZlZWRiYWNrIGRhdGE7CiAgbl9kZXNpZ24gLS0-fCJyZXZpZXcifCBuX2NvZGUKICBuX2NvZGUgLS0-fCJtZXJnZSJ8IG5fYnVpbGQKICBuX2J1aWxkIC0tPnwicHJvbW90ZSJ8IG5fZGVwbG95CiAgbl9kZXBsb3kgLS0-fCJyZWxlYXNlInwgbl9ydW4KICBuX3RocmVhdHMgLS4tPnwiYXR0YWNrInwgbl9ydW4KICBuX3J1biAtLi0-fCJkZXRlY3QifCBuX2ZlZWRiYWNrCiAgbl9mZWVkYmFjayAtLi0-fCJmZWVkIGJhY2sifCBuX2Rlc2lnbg%3FbgColor%3D0d1017%26type%3Dpng" alt="Security is a gate at every stage of the SDLC, not a single checkpoint at the end." width="1562" height="281"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Security is a gate at every stage of the SDLC, not a single checkpoint at the end.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The solid line is the path your code travels. The dashed lines are the security reality: attackers probe the running system constantly, what you detect becomes &lt;em&gt;findings&lt;/em&gt;, and findings flow back into design so the next loop is safer. Security is a cycle, not a finish line.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Design, threat modeling&lt;/strong&gt;: Before writing code, ask "what could go wrong?" Who are the attackers, what are they after, and where are the trust boundaries? A 30-minute conversation here prevents whole categories of bugs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code, secure coding + SAST&lt;/strong&gt;: Developers validate input, avoid dangerous patterns, and let static analysis (SAST) flag risky code automatically in the editor and on every pull request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build, dependency scanning (SCA)&lt;/strong&gt;: Most code in a modern app is &lt;em&gt;not yours&lt;/em&gt;, it's open-source dependencies. Software composition analysis (SCA) checks those libraries for known vulnerabilities before they ship.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy, config and secrets checks&lt;/strong&gt;: Catch the boring-but-fatal mistakes: a public storage bucket, a hardcoded API key, an over-permissioned role, debug mode left on in production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run, monitor and patch&lt;/strong&gt;: Production is where attacks actually happen. Log security events, alert on anomalies, and patch fast when a new vulnerability lands in a dependency you use.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Trust boundaries and attack surface
&lt;/h2&gt;

&lt;p&gt;Two ideas unlock most of security thinking. The first is the &lt;strong&gt;trust boundary&lt;/strong&gt;: the line where data or control crosses from something you trust into something you do not (or vice versa). The browser-to-server line is a trust boundary. So is the line between your service and a third-party API, between your app and the database, and even between two of your own microservices. The rule is simple and absolute: &lt;strong&gt;validate everything that crosses a trust boundary.&lt;/strong&gt; Never trust input just because the &lt;em&gt;last&lt;/em&gt; component was yours.&lt;/p&gt;

&lt;p&gt;The second idea is &lt;strong&gt;attack surface&lt;/strong&gt;: the sum of all the places an attacker can poke at your system, every endpoint, form field, file upload, query parameter, header, open port, and dependency. Every feature you add grows the attack surface. The most reliable way to be more secure is, paradoxically, to &lt;em&gt;have less&lt;/em&gt;: fewer endpoints, fewer dependencies, fewer permissions, less data retained. You cannot be breached through a door you never built.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;A useful instinct:&lt;/strong&gt; Whenever you draw an architecture diagram, draw the trust boundaries on it as dashed lines. Every arrow that crosses a dashed line is a place that needs authentication, authorization, and input validation. This single habit catches an enormous share of real bugs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The principles that hold it all together
&lt;/h2&gt;

&lt;p&gt;AppSec rests on a small set of timeless principles. Learn these four and you have the load-bearing 80%. Everything else, specific attacks, specific tools, is a consequence of them.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Principle&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;th&gt;In practice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CIA, Confidentiality&lt;/td&gt;
&lt;td&gt;Only authorized parties can read the data.&lt;/td&gt;
&lt;td&gt;Encryption in transit (TLS) and at rest; access controls; not logging secrets.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CIA, Integrity&lt;/td&gt;
&lt;td&gt;Data and code cannot be altered by unauthorized parties without detection.&lt;/td&gt;
&lt;td&gt;Checksums, signatures, immutable audit logs, validating input before it's stored.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CIA, Availability&lt;/td&gt;
&lt;td&gt;The system stays up and usable for legitimate users.&lt;/td&gt;
&lt;td&gt;Rate limiting, redundancy, DDoS protection, sane timeouts and resource limits.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Defense in depth&lt;/td&gt;
&lt;td&gt;Multiple independent layers, so one failure isn't fatal.&lt;/td&gt;
&lt;td&gt;WAF + input validation + parameterized queries + least-privilege DB user, all at once.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Least privilege&lt;/td&gt;
&lt;td&gt;Every user, service, and token gets the minimum access it needs.&lt;/td&gt;
&lt;td&gt;Scoped API keys, read-only DB roles, short-lived credentials, no shared admin accounts.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fail secure&lt;/td&gt;
&lt;td&gt;When something breaks, default to denied, not allowed.&lt;/td&gt;
&lt;td&gt;If the auth check errors, reject the request. A crashed gate should be a &lt;em&gt;locked&lt;/em&gt; gate.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The core security principles every engineer should internalize.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The CIA triad, in one breath
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Confidentiality, Integrity, Availability&lt;/strong&gt; are the three things security protects. Confidentiality is keeping secrets secret. Integrity is making sure data is correct and untampered. Availability is keeping the lights on. Every attack you will ever read about violates at least one of these: a data leak breaks confidentiality, a tampered transaction breaks integrity, a ransomware lockout breaks availability. When you assess any risk, ask: &lt;em&gt;which letter of CIA does this threaten?&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Defense in depth and least privilege
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Defense in depth&lt;/strong&gt; is the building analogy made concrete: never rely on one control. If your input validation has a bug, a parameterized query still saves you; if that fails, a least-privileged database user limits the blast radius. &lt;strong&gt;Least privilege&lt;/strong&gt; is the discipline of shrinking that blast radius everywhere, the service account that only needs to read one table should not be able to drop the whole schema. Together they assume &lt;em&gt;something will go wrong&lt;/em&gt; and arrange for it not to be catastrophic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The main domains of AppSec
&lt;/h2&gt;

&lt;p&gt;"Application security" is an umbrella over several connected areas. You do not need to master all of them today, but knowing the map tells you what to learn next.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authentication &amp;amp; authorization&lt;/strong&gt;, proving who a user is, then controlling what they're allowed to do. The two are different, and confusing them causes real breaches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Input validation &amp;amp; injection defense&lt;/strong&gt;, treating all input as hostile so SQL injection, XSS, and command injection have nowhere to land.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secrets management&lt;/strong&gt;, keeping API keys, passwords, and tokens out of source code and configuration, and rotating them safely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependency &amp;amp; supply-chain security&lt;/strong&gt;, knowing what open-source you ship and patching vulnerable libraries before attackers exploit them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data protection &amp;amp; cryptography&lt;/strong&gt;, encryption in transit and at rest, hashing passwords correctly, and not rolling your own crypto.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure configuration&lt;/strong&gt;, hardening servers, containers, and cloud resources so defaults don't leave the door open.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logging, monitoring &amp;amp; incident response&lt;/strong&gt;, detecting attacks, and having a plan for when (not if) one succeeds.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common misconceptions that get people breached
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;"We use HTTPS, so we're secure."&lt;/strong&gt; HTTPS protects data &lt;em&gt;in transit&lt;/em&gt; between browser and server. It does nothing about injection, weak passwords, broken access control, or a leaked API key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Security is the security team's job."&lt;/strong&gt; The team sets standards and tools, but the vulnerabilities live in &lt;em&gt;your&lt;/em&gt; code. Security that isn't owned by developers always loses to the deadline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"We're too small to be a target."&lt;/strong&gt; Most attacks are automated and indiscriminate, bots scan the entire internet for the same known holes. Being small just means no one is watching when it happens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"We'll add security before launch."&lt;/strong&gt; Bolted-on security is expensive, incomplete, and brittle. Shift-left exists precisely because retrofitting is the worst time to do it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Obscurity is protection."&lt;/strong&gt; Hiding an admin page at a secret URL or rolling a homemade encryption scheme is not security. Assume the attacker can read your code, because often they can.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The whole article in seven lines&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AppSec = building software that does the right thing even when someone is attacking it.&lt;/li&gt;
&lt;li&gt;The CIA triad, Confidentiality, Integrity, Availability, is what security protects. Every attack breaks at least one.&lt;/li&gt;
&lt;li&gt;Validate everything that crosses a &lt;strong&gt;trust boundary&lt;/strong&gt;; shrink your &lt;strong&gt;attack surface&lt;/strong&gt; by having less.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Defense in depth&lt;/strong&gt;: layers, so one failure isn't fatal. &lt;strong&gt;Least privilege&lt;/strong&gt;: minimum access everywhere. &lt;strong&gt;Fail secure&lt;/strong&gt;: break to &lt;em&gt;denied&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Security is a gate at every SDLC phase, design, code, build, deploy, run, not a final checkpoint. That's &lt;strong&gt;shift-left&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Security is a developer's job, not just the security team's. The bugs are in your code.&lt;/li&gt;
&lt;li&gt;Most attacks are automated; "too small to be a target" is a myth.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;You now have the mental model. The next articles turn each principle into something you can actually build. Start with the one beginners get wrong most often, then learn the named attacks and how to keep secrets out of your code.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/authentication-vs-authorization" rel="noopener noreferrer"&gt;Authentication vs Authorization&lt;/a&gt;, the difference between &lt;em&gt;who you are&lt;/em&gt; and &lt;em&gt;what you're allowed to do&lt;/em&gt;, and why mixing them up is dangerous.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/the-owasp-top-10-explained" rel="noopener noreferrer"&gt;The OWASP Top 10, Explained&lt;/a&gt;, the canonical list of the most common, most dangerous web vulnerabilities, in plain language.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thesimplifiedtech.com/blog/secrets-management" rel="noopener noreferrer"&gt;Secrets Management&lt;/a&gt;, how to keep API keys, tokens, and passwords out of your source code and config.&lt;/li&gt;
&lt;li&gt;Building toward a security-aware engineering role? The &lt;a href="https://thesimplifiedtech.com/career-paths/devops-engineer" rel="noopener noreferrer"&gt;DevOps Engineer path&lt;/a&gt; weaves these practices into real pipelines and infrastructure.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://thesimplifiedtech.com/blog/what-is-application-security" rel="noopener noreferrer"&gt;TheSimplifiedTech&lt;/a&gt;, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>devsecops</category>
      <category>cybersecurity</category>
      <category>appsec</category>
    </item>
    <item>
      <title>Fine-Tuning LLMs: When and How</title>
      <dc:creator>Sri Balaji</dc:creator>
      <pubDate>Wed, 16 Sep 2026 15:14:02 +0000</pubDate>
      <link>https://dev.to/sri2614/fine-tuning-llms-when-and-how-1g78</link>
      <guid>https://dev.to/sri2614/fine-tuning-llms-when-and-how-1g78</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚡ &lt;strong&gt;TL;DR:&lt;/strong&gt; Fine-tuning changes how a model behaves, not what it knows, so reach for it on style and format, not facts. Learn when prompting or RAG wins instead, and how to build a dataset that actually moves the needle.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The trap everyone falls into&lt;/li&gt;
&lt;li&gt;One principle, one analogy&lt;/li&gt;
&lt;li&gt;The fine-tuning workflow&lt;/li&gt;
&lt;li&gt;Prompting vs RAG vs fine-tuning&lt;/li&gt;
&lt;li&gt;What the data actually looks like&lt;/li&gt;
&lt;li&gt;Evaluating the result&lt;/li&gt;
&lt;li&gt;Common mistakes that cost weeks&lt;/li&gt;
&lt;li&gt;Takeaways&lt;/li&gt;
&lt;li&gt;Where to go next&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The trap everyone falls into
&lt;/h2&gt;

&lt;p&gt;Your model keeps answering in the wrong tone. It ignores your JSON schema. It writes like a chatbot when you need a terse internal tool. So someone says the magic words: &lt;strong&gt;"let's just fine-tune it."&lt;/strong&gt; Three weeks later you have a labeled dataset, a training bill, a checkpoint that is already stale, and the model still hallucinates yesterday's pricing, because fine-tuning was never the tool for that problem.&lt;/p&gt;

&lt;p&gt;Fine-tuning is powerful and badly misunderstood. The single most useful thing you can learn about it is what it &lt;strong&gt;cannot&lt;/strong&gt; do. Get that right and the rest is mechanical.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;Who this is for:&lt;/strong&gt; Engineers who can already call an LLM API and have shipped a prompt or two, maybe even a &lt;a href="https://thesimplifiedtech.com/blog/rag-architecture-explained" rel="noopener noreferrer"&gt;RAG pipeline&lt;/a&gt;, and are now wondering whether fine-tuning is the next step. No ML PhD required. We assume you know what a prompt is and can read JSON and YAML.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  One principle, one analogy
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Fine-tuning teaches a model how to behave. It does not teach it what is true today.&lt;/p&gt;

&lt;p&gt;The rule that prevents 90% of wasted fine-tuning projects&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Think of a base model as a brilliant new graduate. They reason well, write well, and know an enormous amount about the world up to the day they stopped reading. &lt;strong&gt;Fine-tuning is sending them to a finishing school for tone and craft&lt;/strong&gt;, teaching them your house style, your output format, your domain's voice. What it is &lt;em&gt;not&lt;/em&gt; is handing them this morning's newspaper. For fresh facts you put the newspaper on their desk at question time. That desk is your prompt, and the newspaper is &lt;a href="https://thesimplifiedtech.com/blog/rag-architecture-explained" rel="noopener noreferrer"&gt;retrieval&lt;/a&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;In the real world&lt;/th&gt;
&lt;th&gt;In tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Writing a clear brief before each task&lt;/td&gt;
&lt;td&gt;Prompt engineering, cheap, instant, changeable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Giving them the relevant files for this task&lt;/td&gt;
&lt;td&gt;RAG, fresh facts injected at query time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sending them to finishing school for your house style&lt;/td&gt;
&lt;td&gt;Fine-tuning, bakes behavior into the weights&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expecting school to teach today's stock price&lt;/td&gt;
&lt;td&gt;The mistake, weights are frozen at training time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Same employee, three different interventions&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The fine-tuning workflow
&lt;/h2&gt;

&lt;p&gt;When fine-tuning &lt;em&gt;is&lt;/em&gt; the right call, the path is the same every time. Curate a clean dataset, format it the way the trainer expects, train a small adapter (not the whole model), evaluate honestly, deploy, and then watch it in production. The diagram below is the loop; the steps after it walk one pass through it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fY3VyYXRlKCJDdXJhdGUgZGF0YXNldDxici8-Y2xlYW4sIG9uLXRhc2sgZXhhbXBsZXMiKQogIGNsYXNzIG5fY3VyYXRlIGRhdGE7CiAgbl9mb3JtYXQoIkZvcm1hdDxici8-SlNPTkwgY2hhdCBzY2hlbWEiKQogIGNsYXNzIG5fZm9ybWF0IGNvbXB1dGU7CiAgbl90cmFpbigiVHJhaW48YnIvPkxvUkEgYWRhcHRlciIpCiAgY2xhc3Mgbl90cmFpbiBjb21wdXRlOwogIG5fZXZhbCgiRXZhbHVhdGU8YnIvPmhlbGQtb3V0ICsganVkZ2UiKQogIGNsYXNzIG5fZXZhbCBvYnNlcnZhYmlsaXR5OwogIG5fZGVwbG95KCJEZXBsb3k8YnIvPnNlcnZlIGFkYXB0ZXIiKQogIGNsYXNzIG5fZGVwbG95IGVkZ2U7CiAgbl9tb25pdG9yKCJNb25pdG9yPGJyLz5kcmlmdCArIHJlZ3Jlc3Npb25zIikKICBjbGFzcyBuX21vbml0b3Igb2JzZXJ2YWJpbGl0eTsKICBuX2RhdGEoIlByb2R1Y3Rpb24gdHJhZmZpYzxici8-bmV3IGV4YW1wbGVzIikKICBjbGFzcyBuX2RhdGEgZXh0ZXJuYWw7CiAgbl9jdXJhdGUgLS0-IG5fZm9ybWF0CiAgbl9mb3JtYXQgLS0-IG5fdHJhaW4KICBuX3RyYWluIC0tPiBuX2V2YWwKICBuX2V2YWwgLS0-fCJwYXNzZXMgZ2F0ZSJ8IG5fZGVwbG95CiAgbl9ldmFsIC0uLT58ImZhaWxzLCBmaXggZGF0YSJ8IG5fY3VyYXRlCiAgbl9kZXBsb3kgLS4tPiBuX21vbml0b3IKICBuX21vbml0b3IgLS4tPiBuX2RhdGEKICBuX2RhdGEgLS4tPnwibmV4dCByb3VuZCJ8IG5fY3VyYXRl%3FbgColor%3D0d1017%26type%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fY3VyYXRlKCJDdXJhdGUgZGF0YXNldDxici8-Y2xlYW4sIG9uLXRhc2sgZXhhbXBsZXMiKQogIGNsYXNzIG5fY3VyYXRlIGRhdGE7CiAgbl9mb3JtYXQoIkZvcm1hdDxici8-SlNPTkwgY2hhdCBzY2hlbWEiKQogIGNsYXNzIG5fZm9ybWF0IGNvbXB1dGU7CiAgbl90cmFpbigiVHJhaW48YnIvPkxvUkEgYWRhcHRlciIpCiAgY2xhc3Mgbl90cmFpbiBjb21wdXRlOwogIG5fZXZhbCgiRXZhbHVhdGU8YnIvPmhlbGQtb3V0ICsganVkZ2UiKQogIGNsYXNzIG5fZXZhbCBvYnNlcnZhYmlsaXR5OwogIG5fZGVwbG95KCJEZXBsb3k8YnIvPnNlcnZlIGFkYXB0ZXIiKQogIGNsYXNzIG5fZGVwbG95IGVkZ2U7CiAgbl9tb25pdG9yKCJNb25pdG9yPGJyLz5kcmlmdCArIHJlZ3Jlc3Npb25zIikKICBjbGFzcyBuX21vbml0b3Igb2JzZXJ2YWJpbGl0eTsKICBuX2RhdGEoIlByb2R1Y3Rpb24gdHJhZmZpYzxici8-bmV3IGV4YW1wbGVzIikKICBjbGFzcyBuX2RhdGEgZXh0ZXJuYWw7CiAgbl9jdXJhdGUgLS0-IG5fZm9ybWF0CiAgbl9mb3JtYXQgLS0-IG5fdHJhaW4KICBuX3RyYWluIC0tPiBuX2V2YWwKICBuX2V2YWwgLS0-fCJwYXNzZXMgZ2F0ZSJ8IG5fZGVwbG95CiAgbl9ldmFsIC0uLT58ImZhaWxzLCBmaXggZGF0YSJ8IG5fY3VyYXRlCiAgbl9kZXBsb3kgLS4tPiBuX21vbml0b3IKICBuX21vbml0b3IgLS4tPiBuX2RhdGEKICBuX2RhdGEgLS4tPnwibmV4dCByb3VuZCJ8IG5fY3VyYXRl%3FbgColor%3D0d1017%26type%3Dpng" alt="The fine-tuning loop, most of the value is in the first two boxes and the eval gate" width="1598" height="205"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The fine-tuning loop, most of the value is in the first two boxes and the eval gate&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Curate the dataset&lt;/strong&gt;: Collect a few hundred to a few thousand high-quality input/output pairs that demonstrate exactly the behavior you want. Quality beats quantity by a mile, 500 clean examples outperform 5,000 noisy ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Format to the chat schema&lt;/strong&gt;: Convert each pair into the JSONL message format your trainer expects (system / user / assistant turns). Consistency here is non-negotiable; one malformed field can skew the whole run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Train a LoRA adapter&lt;/strong&gt;: Freeze the base model and train a small set of low-rank adapter weights. Minutes to a couple of hours on a single GPU, not a data-center job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evaluate against a held-out set&lt;/strong&gt;: Score on examples the model never saw during training, plus an LLM-judge or human pass for quality. If it does not beat your prompt-only baseline, stop, do not deploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy the adapter&lt;/strong&gt;: Serve the small adapter on top of the base model. You can keep several adapters and swap them per use case without re-hosting the base.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor and feed back&lt;/strong&gt;: Watch for regressions and distribution drift in production, harvest fresh examples, and feed them into the next curation round.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Prompting vs RAG vs fine-tuning
&lt;/h2&gt;

&lt;p&gt;These three are not competitors, they solve different problems and stack happily. The mistake is reaching for the most expensive one first. &lt;strong&gt;Try prompting. Then try RAG. Only then consider fine-tuning.&lt;/strong&gt; Most teams never need the third box.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Prompting&lt;/th&gt;
&lt;th&gt;RAG&lt;/th&gt;
&lt;th&gt;Fine-tuning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Instructions, tone, simple format&lt;/td&gt;
&lt;td&gt;Fresh / private facts&lt;/td&gt;
&lt;td&gt;Consistent style, strict format, narrow domain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Freshness&lt;/td&gt;
&lt;td&gt;Whatever you paste in&lt;/td&gt;
&lt;td&gt;Live, updates with your data&lt;/td&gt;
&lt;td&gt;Frozen at training time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;Near zero&lt;/td&gt;
&lt;td&gt;Low, vector store + retrieval&lt;/td&gt;
&lt;td&gt;High, data prep + GPU + upkeep&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effort to change&lt;/td&gt;
&lt;td&gt;Edit a string&lt;/td&gt;
&lt;td&gt;Update an index&lt;/td&gt;
&lt;td&gt;Re-curate + re-train + re-eval&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time to first result&lt;/td&gt;
&lt;td&gt;Minutes&lt;/td&gt;
&lt;td&gt;Hours to days&lt;/td&gt;
&lt;td&gt;Days to weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Reach for the cheapest tool that solves your actual problem&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;They compose:&lt;/strong&gt; The strongest systems use all three: a fine-tuned model for reliable house-style output, RAG for the facts, and a tight prompt to orchestrate both. Fine-tuning is the last layer you add, not the first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What the data actually looks like
&lt;/h2&gt;

&lt;p&gt;The dataset is the project. Each line is one self-contained example in the chat schema, a system message setting the role, the user input, and the &lt;strong&gt;assistant output you wish the model had produced&lt;/strong&gt;. Below is a tiny slice of a support-triage dataset teaching the model to always reply with a fixed JSON structure.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;train.jsonl&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="nl"&gt;"messages"&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="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"system"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"You are a support triage bot. Reply only with JSON."&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="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"My invoice is wrong and I want a refund."&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="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"assistant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;category&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;billing&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;priority&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;high&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;needs_human&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: true}"&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="nl"&gt;"messages"&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="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"system"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"You are a support triage bot. Reply only with JSON."&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="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"How do I reset my password?"&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="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"assistant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;category&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;account&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;priority&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;low&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;needs_human&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: false}"&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="nl"&gt;"messages"&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="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"system"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"You are a support triage bot. Reply only with JSON."&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="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The app crashes every time I open a report."&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="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"assistant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;category&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;bug&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;priority&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;high&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;needs_human&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: true}"&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;Notice what these examples teach: &lt;strong&gt;format and routing behavior&lt;/strong&gt;, not facts. None of them depend on information that changes day to day. That is the signature of a good fine-tuning dataset.&lt;/p&gt;

&lt;p&gt;Training itself is mostly configuration. A LoRA run reads like this, a base model, a handful of hyperparameters, and the rank that controls how big your adapter is.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;lora.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;base_model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;meta-llama/Llama-3.1-8B-Instruct&lt;/span&gt;
&lt;span class="na"&gt;dataset&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./train.jsonl&lt;/span&gt;

&lt;span class="na"&gt;lora&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;r&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16&lt;/span&gt;              &lt;span class="c1"&gt;# adapter rank, higher = more capacity, more params&lt;/span&gt;
  &lt;span class="na"&gt;alpha&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;32&lt;/span&gt;          &lt;span class="c1"&gt;# scaling factor, usually 2x r&lt;/span&gt;
  &lt;span class="na"&gt;dropout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.05&lt;/span&gt;
  &lt;span class="na"&gt;target_modules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="c1"&gt;# which layers get adapters&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;q_proj&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;v_proj&lt;/span&gt;

&lt;span class="na"&gt;training&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;epochs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;          &lt;span class="c1"&gt;# too many = memorization / overfit&lt;/span&gt;
  &lt;span class="na"&gt;learning_rate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2.0e-4&lt;/span&gt;
  &lt;span class="na"&gt;batch_size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;
  &lt;span class="na"&gt;eval_split&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.1&lt;/span&gt;    &lt;span class="c1"&gt;# hold out 10%, never train on your eval set&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;Why LoRA / PEFT instead of full fine-tuning:&lt;/strong&gt; Full fine-tuning updates every weight in the model, huge memory, huge cost, and a fresh multi-gigabyte checkpoint each time. &lt;strong&gt;Parameter-efficient fine-tuning (PEFT)&lt;/strong&gt; methods like &lt;strong&gt;LoRA&lt;/strong&gt; freeze the base model and train a tiny set of low-rank adapter matrices, often under 1% of the parameters. You get most of the benefit for a fraction of the cost, the adapters are megabytes not gigabytes, and you can host one base model with many swappable adapters.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Evaluating the result
&lt;/h2&gt;

&lt;p&gt;A fine-tune that you did not evaluate is a liability, not an asset. The non-negotiable rule: &lt;strong&gt;always compare against your prompt-only baseline on data the model never trained on.&lt;/strong&gt; If the fine-tune does not clearly beat the baseline, you just spent weeks to ship a regression risk.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hold out a test set early.&lt;/strong&gt; Split before you train and never let those examples leak into training.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Beat the baseline or stop.&lt;/strong&gt; Run the same eval against plain prompting and against RAG. The fine-tune has to win to justify itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure what you actually fine-tuned for.&lt;/strong&gt; Format compliance, tone match, schema validity, task-specific metrics, not a generic benchmark score.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch for regressions.&lt;/strong&gt; A model tuned hard on one task often gets &lt;em&gt;worse&lt;/em&gt; at everything else. Keep a small general-capability check in the loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use an LLM judge for the fuzzy stuff&lt;/strong&gt;, but spot-check the judge against human ratings so you trust it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the full discipline of measuring LLM systems, judges, regression suites, and offline-vs-online evals, see &lt;a href="https://thesimplifiedtech.com/blog/evaluating-llm-applications" rel="noopener noreferrer"&gt;Evaluating LLM Applications&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes that cost weeks
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fine-tuning for fresh facts.&lt;/strong&gt; Weights freeze at training time. If the answer changes daily, you want RAG, not a fine-tune. This is the number-one wasted project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Too little or dirty data.&lt;/strong&gt; A few dozen inconsistent examples teach the model noise. Curate for quality; 500 clean pairs beat 5,000 messy ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No evaluation.&lt;/strong&gt; Shipping a checkpoint because the loss curve looked nice, with no held-out comparison to the baseline. You have no idea if it is better.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skipping prompting and RAG first.&lt;/strong&gt; Reaching straight for the most expensive, slowest, hardest-to-change tool when a one-line prompt edit or a retrieval step would have solved it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overfitting on epochs.&lt;/strong&gt; Training too long memorizes your examples and destroys generalization. Watch eval loss, not just training loss.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One model to rule them all.&lt;/strong&gt; Cramming many unrelated tasks into one fine-tune. Prefer small, focused adapters you can swap per use case.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The whole article in seven lines&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fine-tuning changes &lt;strong&gt;behavior&lt;/strong&gt; (style, tone, format), never &lt;strong&gt;knowledge&lt;/strong&gt; of fresh facts.&lt;/li&gt;
&lt;li&gt;Try &lt;strong&gt;prompting first&lt;/strong&gt;, then &lt;strong&gt;RAG&lt;/strong&gt;, and only then fine-tuning. Most teams never need the third.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;RAG&lt;/strong&gt; for anything that changes; use fine-tuning for consistent house style and strict formats.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;dataset is the project&lt;/strong&gt;, a few hundred clean, on-task examples beat thousands of noisy ones.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;LoRA / PEFT&lt;/strong&gt;, train tiny adapters, not the whole model. Cheaper, faster, swappable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always evaluate&lt;/strong&gt; on a held-out set against your prompt-only baseline. No win, no deploy.&lt;/li&gt;
&lt;li&gt;Then &lt;strong&gt;monitor&lt;/strong&gt; in production and feed new examples into the next round.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;Fine-tuning sits near the top of the AI engineering stack, reach for it after you have squeezed prompting and retrieval. Build the layers underneath it first.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Solve the freshness problem the right way: &lt;a href="https://thesimplifiedtech.com/blog/rag-architecture-explained" rel="noopener noreferrer"&gt;RAG Architecture Explained&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Build the eval discipline before you train anything: &lt;a href="https://thesimplifiedtech.com/blog/evaluating-llm-applications" rel="noopener noreferrer"&gt;Evaluating LLM Applications&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;See where fine-tuning fits in the broader role: the &lt;a href="https://thesimplifiedtech.com/career-paths/ai-engineer" rel="noopener noreferrer"&gt;AI Engineer career path&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://thesimplifiedtech.com/blog/fine-tuning-llms-when-and-how" rel="noopener noreferrer"&gt;TheSimplifiedTech&lt;/a&gt;, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>llm</category>
      <category>finetuning</category>
    </item>
    <item>
      <title>LLMOps: Productionizing LLM Apps</title>
      <dc:creator>Sri Balaji</dc:creator>
      <pubDate>Tue, 15 Sep 2026 15:21:35 +0000</pubDate>
      <link>https://dev.to/sri2614/llmops-productionizing-llm-apps-2jpo</link>
      <guid>https://dev.to/sri2614/llmops-productionizing-llm-apps-2jpo</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚡ &lt;strong&gt;TL;DR:&lt;/strong&gt; LLMOps is DevOps for non-deterministic software. The five ops concerns that keep a model from going off-script at 2am: versioned prompts, tracing, guardrails, fallbacks, and drift monitoring.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It is 2am and the model is off-script&lt;/li&gt;
&lt;li&gt;LLMOps is DevOps for non-deterministic software&lt;/li&gt;
&lt;li&gt;The picture: an LLMOps pipeline&lt;/li&gt;
&lt;li&gt;Five ops concerns, translated&lt;/li&gt;
&lt;li&gt;Wrapping a call with tracing and a guardrail&lt;/li&gt;
&lt;li&gt;Common mistakes that page you at 2am&lt;/li&gt;
&lt;li&gt;Takeaways&lt;/li&gt;
&lt;li&gt;Where to go next&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  It is 2am and the model is off-script
&lt;/h2&gt;

&lt;p&gt;The demo went perfectly. You typed a question, the model answered like a senior colleague, the room nodded, and the feature shipped. Three weeks later it is 2am and your phone is buzzing. A user pasted a screenshot into your support chat: your friendly assistant has cheerfully recommended a competitor, leaked a chunk of someone else's order history, and signed off with a joke that will not read well on social media. Nothing crashed. No exception was thrown. The system did exactly what it was told, it just should not have been told that.&lt;/p&gt;

&lt;p&gt;This is the gap nobody mentions in the demo. Shipping an LLM &lt;strong&gt;call&lt;/strong&gt; is easy. Running an LLM &lt;strong&gt;feature&lt;/strong&gt;, one that stays safe, stays good, and stays debuggable as prompts change and the model drifts under you, is a different discipline. That discipline is &lt;strong&gt;LLMOps&lt;/strong&gt;: the operational practices that turn a clever prompt into software you can sleep through the night with.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;Who this is for:&lt;/strong&gt; Engineers who have a working LLM prototype and now have to make it production-grade. You know how to call an API and write a prompt. You have not yet been paged because the model said something it should not have. This article is the playbook for the layer between "it works on my machine" and "it is safe in front of users."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  LLMOps is DevOps for non-deterministic software
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;LLMOps is the practice of treating prompts, models, and their outputs as production artifacts, versioned, observed, guarded, and continuously evaluated, exactly because the software underneath is non-deterministic.&lt;/p&gt;

&lt;p&gt;The working definition&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here is the mental model. Traditional software is a machine: same input, same output, every time. You test it once and trust it forever. An LLM feature is more like a very capable, very literal new hire who occasionally has a bad day. You cannot unit-test a personality. What you can do is give them a written playbook (a versioned prompt), put a reviewer on the door for anything that goes in and out (guardrails), record every conversation so you can review what happened (tracing), and quietly score a sample of their work each week to catch the day their quality slips (drift monitoring).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;In the real world&lt;/th&gt;
&lt;th&gt;In tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Source code in Git, tagged releases&lt;/td&gt;
&lt;td&gt;Prompts versioned and pinned per deploy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI test suite gates the merge&lt;/td&gt;
&lt;td&gt;Eval suite gates the prompt change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;APM traces every request through services&lt;/td&gt;
&lt;td&gt;Tracing captures every LLM call: prompt, tokens, latency, cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A WAF inspects traffic at the edge&lt;/td&gt;
&lt;td&gt;Guardrails validate model input and output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alert when error rate creeps up&lt;/td&gt;
&lt;td&gt;Alert when answer quality drifts down&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;You already have the muscles for this, LLMOps just points them at non-deterministic software.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you have done DevOps and observability, you have the instincts. The twist is that your unit of work is probabilistic, so "correct" is a distribution rather than a checkmark, and that changes how you version, test, monitor, and roll back. Everything below is one of those five concerns, translated.&lt;/p&gt;

&lt;h2&gt;
  
  
  The picture: an LLMOps pipeline
&lt;/h2&gt;

&lt;p&gt;Every production LLM request runs a gauntlet. It is not "user text in, model text out", there is a checkpoint before the model and a checkpoint after it, and everything that happens in between is recorded for later review and scoring.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fcmVxKCJSZXF1ZXN0PGJyLz5Vc2VyIGlucHV0IikKICBjbGFzcyBuX3JlcSBjbGllbnQ7CiAgbl9pbmd1YXJkKCJJbnB1dCBHdWFyZHJhaWw8YnIvPlBJSSDCtyBpbmplY3Rpb24gwrcgcG9saWN5IikKICBjbGFzcyBuX2luZ3VhcmQgZWRnZTsKICBuX3Byb21wdCgiUHJvbXB0PGJyLz5WZXJzaW9uZWQgdGVtcGxhdGUiKQogIGNsYXNzIG5fcHJvbXB0IGNvbXB1dGU7CiAgbl9tb2RlbCgiTW9kZWw8YnIvPkxMTSBjYWxsIikKICBjbGFzcyBuX21vZGVsIGNvbXB1dGU7CiAgbl9vdXRndWFyZCgiT3V0cHV0IEd1YXJkcmFpbDxici8-dmFsaWRhdGUgwrcgcmVkYWN0IMK3IHJlZnVzZSIpCiAgY2xhc3Mgbl9vdXRndWFyZCBlZGdlOwogIG5fcmVzcCgiUmVzcG9uc2U8YnIvPlNhZmUgb3V0cHV0IikKICBjbGFzcyBuX3Jlc3AgY2xpZW50OwogIG5fdHJhY2UoIlRyYWNpbmc8YnIvPnNwYW5zIMK3IHRva2VucyDCtyBjb3N0IikKICBjbGFzcyBuX3RyYWNlIG9ic2VydmFiaWxpdHk7CiAgbl9ldmFsKCJFdmFsIExvb3A8YnIvPnNjb3JlZCBzYW1wbGUiKQogIGNsYXNzIG5fZXZhbCBvYnNlcnZhYmlsaXR5OwogIG5fcmVxIC0tPiBuX2luZ3VhcmQKICBuX2luZ3VhcmQgLS0-IG5fcHJvbXB0CiAgbl9wcm9tcHQgLS0-IG5fbW9kZWwKICBuX21vZGVsIC0tPiBuX291dGd1YXJkCiAgbl9vdXRndWFyZCAtLT4gbl9yZXNwCiAgbl9wcm9tcHQgLS4tPnwiZW1pdCBzcGFuInwgbl90cmFjZQogIG5fbW9kZWwgLS4tPnwiZW1pdCBzcGFuInwgbl90cmFjZQogIG5fb3V0Z3VhcmQgLS4tPnwic2FtcGxlInwgbl9ldmFs%3FbgColor%3D0d1017%26type%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fcmVxKCJSZXF1ZXN0PGJyLz5Vc2VyIGlucHV0IikKICBjbGFzcyBuX3JlcSBjbGllbnQ7CiAgbl9pbmd1YXJkKCJJbnB1dCBHdWFyZHJhaWw8YnIvPlBJSSDCtyBpbmplY3Rpb24gwrcgcG9saWN5IikKICBjbGFzcyBuX2luZ3VhcmQgZWRnZTsKICBuX3Byb21wdCgiUHJvbXB0PGJyLz5WZXJzaW9uZWQgdGVtcGxhdGUiKQogIGNsYXNzIG5fcHJvbXB0IGNvbXB1dGU7CiAgbl9tb2RlbCgiTW9kZWw8YnIvPkxMTSBjYWxsIikKICBjbGFzcyBuX21vZGVsIGNvbXB1dGU7CiAgbl9vdXRndWFyZCgiT3V0cHV0IEd1YXJkcmFpbDxici8-dmFsaWRhdGUgwrcgcmVkYWN0IMK3IHJlZnVzZSIpCiAgY2xhc3Mgbl9vdXRndWFyZCBlZGdlOwogIG5fcmVzcCgiUmVzcG9uc2U8YnIvPlNhZmUgb3V0cHV0IikKICBjbGFzcyBuX3Jlc3AgY2xpZW50OwogIG5fdHJhY2UoIlRyYWNpbmc8YnIvPnNwYW5zIMK3IHRva2VucyDCtyBjb3N0IikKICBjbGFzcyBuX3RyYWNlIG9ic2VydmFiaWxpdHk7CiAgbl9ldmFsKCJFdmFsIExvb3A8YnIvPnNjb3JlZCBzYW1wbGUiKQogIGNsYXNzIG5fZXZhbCBvYnNlcnZhYmlsaXR5OwogIG5fcmVxIC0tPiBuX2luZ3VhcmQKICBuX2luZ3VhcmQgLS0-IG5fcHJvbXB0CiAgbl9wcm9tcHQgLS0-IG5fbW9kZWwKICBuX21vZGVsIC0tPiBuX291dGd1YXJkCiAgbl9vdXRndWFyZCAtLT4gbl9yZXNwCiAgbl9wcm9tcHQgLS4tPnwiZW1pdCBzcGFuInwgbl90cmFjZQogIG5fbW9kZWwgLS4tPnwiZW1pdCBzcGFuInwgbl90cmFjZQogIG5fb3V0Z3VhcmQgLS4tPnwic2FtcGxlInwgbl9ldmFs%3FbgColor%3D0d1017%26type%3Dpng" alt="A request passes an input guardrail, hits a versioned prompt and the model, then an output guardrail before the response" width="1293" height="299"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A request passes an input guardrail, hits a versioned prompt and the model, then an output guardrail before the response. Every hop is traced; a sampled copy feeds the eval loop.&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Request arrives&lt;/strong&gt;: Raw user input enters the pipeline. Treat it as untrusted, it may contain PII, a prompt injection payload, or something that violates your policy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Input guardrail&lt;/strong&gt;: Before a single token reaches the model: strip or block PII, scan for jailbreak and injection patterns, and reject inputs that fail policy. Fail fast and cheap, before you pay for inference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Versioned prompt&lt;/strong&gt;: The cleaned input is rendered into a prompt template pinned to a specific version (e.g. support-agent@v7). The version is part of the deploy, not a string buried in a function.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model call&lt;/strong&gt;: The composed prompt hits the LLM. This hop is wrapped in a trace span that records model id, prompt version, token counts, latency, and cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output guardrail&lt;/strong&gt;: The raw completion is validated before anyone sees it: enforce schema, redact leaked PII, check for policy violations, and refuse or repair if it fails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Response + feedback&lt;/strong&gt;: The safe output returns to the user. A sampled, traced copy flows to the eval loop, where it is scored over time so you can see quality drift before your users do.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Five ops concerns, translated
&lt;/h2&gt;

&lt;p&gt;The fastest way to internalize LLMOps is to map it onto the operations you already run. Same five concerns, versioning, testing, monitoring, rollback, security, with an LLM-shaped answer for each.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;Traditional ops&lt;/th&gt;
&lt;th&gt;LLMOps equivalent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Versioning&lt;/td&gt;
&lt;td&gt;Tagged code releases in Git&lt;/td&gt;
&lt;td&gt;Prompts and model ids pinned and versioned per deploy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testing&lt;/td&gt;
&lt;td&gt;Deterministic unit and integration tests&lt;/td&gt;
&lt;td&gt;Eval suites scoring outputs against a graded dataset&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monitoring&lt;/td&gt;
&lt;td&gt;Latency, error rate, throughput&lt;/td&gt;
&lt;td&gt;Quality scores, refusal rate, token cost, drift over time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rollback&lt;/td&gt;
&lt;td&gt;Redeploy the previous build&lt;/td&gt;
&lt;td&gt;Re-pin the previous prompt version or model snapshot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security&lt;/td&gt;
&lt;td&gt;AuthN/Z, input validation, WAF&lt;/td&gt;
&lt;td&gt;Guardrails: PII handling, jailbreak and injection defense, output validation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Traditional ops concern mapped to its LLMOps equivalent.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Non-determinism breaks the green checkmark:&lt;/strong&gt; Your eval suite will not return a clean pass/fail like a unit test does. It returns scores on a sample, and those scores have variance. Gate on thresholds and trends ("faithfulness must stay above 0.85, and not drop more than 5 points week over week"), not on exact equality. Read &lt;a href="https://thesimplifiedtech.com/blog/evaluating-llm-applications" rel="noopener noreferrer"&gt;Evaluating LLM Applications&lt;/a&gt; for how to build the graded dataset and judges this depends on.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Wrapping a call with tracing and a guardrail
&lt;/h2&gt;

&lt;p&gt;Here is the smallest version of the pipeline that is still real: one LLM call, wrapped so that it is traced end to end and so that nothing leaves without passing an output guardrail. Notice the prompt is &lt;strong&gt;pinned to a version&lt;/strong&gt;, the whole call is &lt;strong&gt;inside a trace span&lt;/strong&gt;, and the output is &lt;strong&gt;validated before it is returned&lt;/strong&gt;, a failed check raises rather than silently shipping a bad answer.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;llm_call.py&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;prompts&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;get_prompt&lt;/span&gt;          &lt;span class="c1"&gt;# returns (text, version)
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;call_model&lt;/span&gt;            &lt;span class="c1"&gt;# thin wrapper over your provider SDK
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;tracing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tracer&lt;/span&gt;               &lt;span class="c1"&gt;# OpenTelemetry-style tracer
&lt;/span&gt;
&lt;span class="n"&gt;PII_PATTERN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\b\d{3}-\d{2}-\d{4}\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# naive SSN example
&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GuardrailError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Raised when an output fails validation so it never reaches the user.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;output_guardrail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# 1. Refuse if the model leaked something it should not have.
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;PII_PATTERN&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;GuardrailError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output contained PII&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# 2. Enforce a minimum-quality contract (e.g. non-empty, bounded length).
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;GuardrailError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output failed length/format policy&lt;/span&gt;&lt;span class="sh"&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;text&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;prompt_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;support-agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;request_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_as_current_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llm.answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;request.id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt.version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt_version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;composed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prompt_text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user_input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;start&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="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;call_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;composed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="c1"&gt;# -&amp;gt; {text, model, tokens}
&lt;/span&gt;        &lt;span class="n"&gt;latency_ms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&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="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;

        &lt;span class="c1"&gt;# Observability: the data you will wish you had at 2am.
&lt;/span&gt;        &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model.id&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model.tokens&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latency.ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latency_ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;safe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;output_guardrail&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;guardrail.passed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&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;safe&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;GuardrailError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Do not ship the unsafe output. Fall back, and record why.
&lt;/span&gt;            &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;guardrail.passed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;guardrail.reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Sorry, I cannot help with that right now.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;That fallback at the end is the unglamorous hero of production LLM work. When the guardrail trips, or when the model times out, or the provider returns a 500, you return a safe, boring response and log the reason, instead of leaking the failure to the user. A real system layers fallbacks: retry, then a cheaper backup model, then a canned safe answer. The point is that the user never sees the raw failure, and you always see the trace.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Do not build guardrails entirely from regex:&lt;/strong&gt; The PII regex above is a teaching stub. In production, combine deterministic checks (schema validation, allowlists, format contracts) with a dedicated classifier or a small judge model for fuzzy concerns like toxicity, jailbreak intent, and topic policy. Cheap deterministic checks first, expensive model-based checks only when needed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Common mistakes that page you at 2am
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Unversioned prompts.&lt;/strong&gt; The prompt lives as a string literal in a function, someone tweaks it to fix one edge case, and now you cannot tell which deploy produced last week's bad answers, or roll back to the version that was fine. Pin every prompt to a version and ship the version with the deploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No tracing.&lt;/strong&gt; When a user reports a bad answer and you have no record of the exact prompt, model, tokens, and output for that request, you are debugging blind. You cannot reproduce what you did not capture. Trace every call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No guardrails.&lt;/strong&gt; Trusting the model to police its own input and output is how PII leaks, prompt injections succeed, and off-policy text reaches users. Validate what goes in and what comes out, assume both are hostile.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No drift monitoring.&lt;/strong&gt; Quality does not fail loudly. The model provider silently updates a snapshot, your data distribution shifts, an upstream prompt changes, and your answers get a little worse every week until someone notices on Twitter. Score a sample continuously and alert on the trend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating eval as a one-time gate.&lt;/strong&gt; Passing evals before launch tells you nothing about month three. The eval loop has to run continuously on production traffic, not once in CI.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;LLMOps in nine lines&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Shipping an LLM call is easy; running an LLM feature safely is a discipline, LLMOps.&lt;/li&gt;
&lt;li&gt;It is DevOps and observability for non-deterministic software: version, test, monitor, roll back, secure.&lt;/li&gt;
&lt;li&gt;Every request runs a gauntlet: input guardrail → versioned prompt → model → output guardrail → response.&lt;/li&gt;
&lt;li&gt;Version prompts and model ids per deploy so you can pin, diff, and roll back.&lt;/li&gt;
&lt;li&gt;Trace every call, prompt version, model id, tokens, latency, cost, or you will debug blind.&lt;/li&gt;
&lt;li&gt;Guardrails validate input and output: PII, jailbreak and injection defense, schema and policy.&lt;/li&gt;
&lt;li&gt;Always have a fallback: retry, backup model, then a safe canned answer. Never leak the failure.&lt;/li&gt;
&lt;li&gt;Gate on thresholds and trends, not exact equality, quality is a distribution, not a checkmark.&lt;/li&gt;
&lt;li&gt;Monitor drift continuously on sampled production traffic; quality fails quietly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;LLMOps stands on two skills you can deepen right now. The guardrails and prompts you version are only as good as the prompts themselves, and the drift you monitor is only measurable if you can score quality at all.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build the scoring foundation that drift monitoring and eval gates depend on: &lt;a href="https://thesimplifiedtech.com/blog/evaluating-llm-applications" rel="noopener noreferrer"&gt;Evaluating LLM Applications&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Write prompts worth versioning in the first place: &lt;a href="https://thesimplifiedtech.com/blog/prompt-engineering-fundamentals" rel="noopener noreferrer"&gt;Prompt Engineering Fundamentals&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;See where this fits in the broader role and what to learn next: the &lt;a href="https://thesimplifiedtech.com/career-paths/ai-engineer" rel="noopener noreferrer"&gt;AI Engineer career path&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start small: pick your one live LLM feature, pin its prompt to a version, wrap the call in a trace, and add a single output guardrail. That is the minimum that lets you debug the next 2am page instead of guessing through it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://thesimplifiedtech.com/blog/llmops-productionizing-llm-apps" rel="noopener noreferrer"&gt;TheSimplifiedTech&lt;/a&gt;, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>llm</category>
      <category>llmops</category>
    </item>
    <item>
      <title>LLM Cost &amp; Latency Optimization</title>
      <dc:creator>Sri Balaji</dc:creator>
      <pubDate>Mon, 14 Sep 2026 16:43:19 +0000</pubDate>
      <link>https://dev.to/sri2614/llm-cost-latency-optimization-5h71</link>
      <guid>https://dev.to/sri2614/llm-cost-latency-optimization-5h71</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚡ &lt;strong&gt;TL;DR:&lt;/strong&gt; The cheapest token is the one you never send. A practical playbook for cutting the bill and the p95: token budgeting, caching, model routing to smaller models, and streaming so apps stay cheap and fast enough to ship.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Your demo worked. The bill and the p95 did not.&lt;/li&gt;
&lt;li&gt;The principle: don't send the token in the first place&lt;/li&gt;
&lt;li&gt;The picture: a request's journey to the cheapest answer&lt;/li&gt;
&lt;li&gt;The levers, and what each one actually buys you&lt;/li&gt;
&lt;li&gt;A minimal router and token-budget trimmer&lt;/li&gt;
&lt;li&gt;Common mistakes that quietly drain the budget&lt;/li&gt;
&lt;li&gt;Takeaways&lt;/li&gt;
&lt;li&gt;Where to go next&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Your demo worked. The bill and the p95 did not.
&lt;/h2&gt;

&lt;p&gt;The demo was magic. You wired up a model, pasted in a fat system prompt, stuffed the whole knowledge base into the context, and it answered beautifully. Then you shipped it to real traffic. Two things broke at once: the &lt;strong&gt;monthly bill&lt;/strong&gt; climbed past what the feature is worth, and the &lt;strong&gt;p95 latency&lt;/strong&gt;, the slow-tail experience your impatient users actually feel, crept toward six, eight, ten seconds. Suddenly the magic feels like a liability.&lt;/p&gt;

&lt;p&gt;Here is the good news: almost none of this requires a smarter model. It requires &lt;em&gt;engineering&lt;/em&gt;. The same disciplines you already apply to databases and APIs, caching, routing, trimming, batching, map cleanly onto LLM calls. This article is the playbook: the levers that make an LLM app cheap and fast enough to keep in production, in the order you should reach for them.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;Who this is for:&lt;/strong&gt; Engineers who have an LLM feature &lt;em&gt;working&lt;/em&gt; and now need it to be &lt;strong&gt;affordable and responsive&lt;/strong&gt; at scale. You should be comfortable reading Python and thinking about percentiles and per-request cost. No ML background required, this is systems work, not model training. New to how these models tick? Start with &lt;a href="https://thesimplifiedtech.com/blog/how-llms-actually-work" rel="noopener noreferrer"&gt;How LLMs Actually Work&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The principle: don't send the token in the first place
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;The cheapest, fastest token is the one you never send.&lt;/p&gt;

&lt;p&gt;The whole article in one line&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every optimization here is a variation on that idea. You pay, in dollars &lt;em&gt;and&lt;/em&gt; in milliseconds, roughly in proportion to the tokens that flow in and out of the model. &lt;strong&gt;Input tokens&lt;/strong&gt; cost money to send and add to the time-to-first-token. &lt;strong&gt;Output tokens&lt;/strong&gt; cost more per unit &lt;em&gt;and&lt;/em&gt; are generated one at a time, so they dominate latency. So the entire game is: send fewer tokens, generate fewer tokens, and avoid the call entirely when you can.&lt;/p&gt;

&lt;p&gt;Crucially, optimizing cost and optimizing latency are not always the same move. Some levers buy you both (a cache hit is free &lt;em&gt;and&lt;/em&gt; instant). Some buy one at the expense of the other (batching is cheaper but slower per request). Knowing which is which is what lets you tune deliberately instead of flailing.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;In the real world&lt;/th&gt;
&lt;th&gt;In tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A pricey consultant who bills by the word&lt;/td&gt;
&lt;td&gt;The LLM, every input and output token costs money and time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checking your notes before booking the meeting&lt;/td&gt;
&lt;td&gt;A cache lookup before calling the model at all&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Asking the junior analyst the easy questions first&lt;/td&gt;
&lt;td&gt;Routing simple requests to a small, cheap model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sending a one-page brief instead of the whole filing cabinet&lt;/td&gt;
&lt;td&gt;Trimming the prompt and context to what's relevant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The consultant talking while they think, not after&lt;/td&gt;
&lt;td&gt;Streaming tokens so the user sees output immediately&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Treat the model like an expensive specialist consultant, not a search box you spam.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The picture: a request's journey to the cheapest answer
&lt;/h2&gt;

&lt;p&gt;Before reaching for any single model, a well-built LLM service runs each request through a funnel. Every stage is a chance to answer &lt;em&gt;cheaper&lt;/em&gt; or &lt;em&gt;not at all&lt;/em&gt;. Only the requests that survive every filter reach the expensive large model.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fY2xpZW50KCJDbGllbnQ8YnIvPlVzZXIgcmVxdWVzdCIpCiAgY2xhc3Mgbl9jbGllbnQgY2xpZW50OwogIG5fY2FjaGUoIlNlbWFudGljIENhY2hlPGJyLz5FbWJlZGRpbmcgc2ltaWxhcml0eSIpCiAgY2xhc3Mgbl9jYWNoZSBkYXRhOwogIG5fcm91dGVyKCJSb3V0ZXI8YnIvPkNsYXNzaWZ5IGRpZmZpY3VsdHkiKQogIGNsYXNzIG5fcm91dGVyIGNvbXB1dGU7CiAgbl9zbWFsbCgiU21hbGwgTW9kZWw8YnIvPkNoZWFwICYgZmFzdCIpCiAgY2xhc3Mgbl9zbWFsbCBjb21wdXRlOwogIG5fbGFyZ2UoIkxhcmdlIE1vZGVsPGJyLz5Fc2NhbGF0aW9uIG9ubHkiKQogIGNsYXNzIG5fbGFyZ2UgZXh0ZXJuYWw7CiAgbl9yZXNwKCJSZXNwb25zZTxici8-U3RyZWFtZWQgdG9rZW5zIikKICBjbGFzcyBuX3Jlc3AgY2xpZW50OwogIG5fY2xpZW50IC0tPnwiYXNrInwgbl9jYWNoZQogIG5fY2FjaGUgLS4tPnwiY2FjaGUgaGl0Inwgbl9yZXNwCiAgbl9jYWNoZSAtLT58Im1pc3MifCBuX3JvdXRlcgogIG5fcm91dGVyIC0tPnwiZWFzeSJ8IG5fc21hbGwKICBuX3JvdXRlciAtLT58ImhhcmQifCBuX2xhcmdlCiAgbl9zbWFsbCAtLT58InN0cmVhbSJ8IG5fcmVzcAogIG5fbGFyZ2UgLS0-fCJzdHJlYW0ifCBuX3Jlc3AKICBuX3Jlc3AgLS4tPnwid3JpdGUtYmFjayJ8IG5fY2FjaGU%3FbgColor%3D0d1017%26type%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fY2xpZW50KCJDbGllbnQ8YnIvPlVzZXIgcmVxdWVzdCIpCiAgY2xhc3Mgbl9jbGllbnQgY2xpZW50OwogIG5fY2FjaGUoIlNlbWFudGljIENhY2hlPGJyLz5FbWJlZGRpbmcgc2ltaWxhcml0eSIpCiAgY2xhc3Mgbl9jYWNoZSBkYXRhOwogIG5fcm91dGVyKCJSb3V0ZXI8YnIvPkNsYXNzaWZ5IGRpZmZpY3VsdHkiKQogIGNsYXNzIG5fcm91dGVyIGNvbXB1dGU7CiAgbl9zbWFsbCgiU21hbGwgTW9kZWw8YnIvPkNoZWFwICYgZmFzdCIpCiAgY2xhc3Mgbl9zbWFsbCBjb21wdXRlOwogIG5fbGFyZ2UoIkxhcmdlIE1vZGVsPGJyLz5Fc2NhbGF0aW9uIG9ubHkiKQogIGNsYXNzIG5fbGFyZ2UgZXh0ZXJuYWw7CiAgbl9yZXNwKCJSZXNwb25zZTxici8-U3RyZWFtZWQgdG9rZW5zIikKICBjbGFzcyBuX3Jlc3AgY2xpZW50OwogIG5fY2xpZW50IC0tPnwiYXNrInwgbl9jYWNoZQogIG5fY2FjaGUgLS4tPnwiY2FjaGUgaGl0Inwgbl9yZXNwCiAgbl9jYWNoZSAtLT58Im1pc3MifCBuX3JvdXRlcgogIG5fcm91dGVyIC0tPnwiZWFzeSJ8IG5fc21hbGwKICBuX3JvdXRlciAtLT58ImhhcmQifCBuX2xhcmdlCiAgbl9zbWFsbCAtLT58InN0cmVhbSJ8IG5fcmVzcAogIG5fbGFyZ2UgLS0-fCJzdHJlYW0ifCBuX3Jlc3AKICBuX3Jlc3AgLS4tPnwid3JpdGUtYmFjayJ8IG5fY2FjaGU%3FbgColor%3D0d1017%26type%3Dpng" alt="A request flows through a semantic cache and a router before ever touching a model; streaming carries the answer back to" width="1146" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A request flows through a semantic cache and a router before ever touching a model; streaming carries the answer back token-by-token.&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check the semantic cache&lt;/strong&gt;: Embed the incoming request and look for a near-identical question answered before. On a hit, return the stored answer instantly, zero model tokens, single-digit-millisecond latency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Route on a cache miss&lt;/strong&gt;: A lightweight classifier (rules, a tiny model, or a heuristic) decides whether this is an easy request or a hard one. Most production traffic is easy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Try the small model first&lt;/strong&gt;: Send easy requests to a small, cheap, fast model. It handles the long tail of simple lookups, classifications, and short rewrites for a fraction of the cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Escalate only when needed&lt;/strong&gt;: Hard requests, or small-model answers that fail a confidence/validation check, escalate to the large model. You pay the premium only for the requests that truly need it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stream the answer back&lt;/strong&gt;: Whichever model answers, stream tokens to the client as they generate. Total time is unchanged, but perceived latency collapses because the user sees words immediately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write back to the cache&lt;/strong&gt;: Store the answer keyed by the request embedding so the next similar question skips the whole funnel.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The levers, and what each one actually buys you
&lt;/h2&gt;

&lt;p&gt;These are the six moves worth knowing, ranked roughly by impact-per-effort. The honest framing matters: each lever has a trade-off, and stacking them blindly can hurt. Read the table as a menu, not a checklist.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Lever&lt;/th&gt;
&lt;th&gt;Saves cost?&lt;/th&gt;
&lt;th&gt;Saves latency?&lt;/th&gt;
&lt;th&gt;Trade-off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Exact / prompt caching&lt;/td&gt;
&lt;td&gt;Yes, cached input is heavily discounted&lt;/td&gt;
&lt;td&gt;Yes, skips reprocessing&lt;/td&gt;
&lt;td&gt;Only helps on repeated prefixes; needs stable prompt ordering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Semantic caching&lt;/td&gt;
&lt;td&gt;Yes, full call avoided on hit&lt;/td&gt;
&lt;td&gt;Yes, instant on hit&lt;/td&gt;
&lt;td&gt;Risk of serving a stale or subtly-wrong match; tune the threshold&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model routing (small first)&lt;/td&gt;
&lt;td&gt;Yes, most traffic on cheap model&lt;/td&gt;
&lt;td&gt;Yes, small models respond faster&lt;/td&gt;
&lt;td&gt;Routing mistakes send hard queries to a weak model; needs a fallback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt / context trimming&lt;/td&gt;
&lt;td&gt;Yes, fewer input tokens&lt;/td&gt;
&lt;td&gt;Yes, less to process&lt;/td&gt;
&lt;td&gt;Trim too aggressively and you cut the context the answer needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batching requests&lt;/td&gt;
&lt;td&gt;Yes, better throughput per dollar&lt;/td&gt;
&lt;td&gt;No, adds queueing delay&lt;/td&gt;
&lt;td&gt;Wrong for interactive UX; great for offline / bulk jobs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Smaller model outright&lt;/td&gt;
&lt;td&gt;Yes, lower per-token price&lt;/td&gt;
&lt;td&gt;Yes, faster generation&lt;/td&gt;
&lt;td&gt;Quality drop on complex tasks; verify on your eval set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Streaming&lt;/td&gt;
&lt;td&gt;No, same tokens billed&lt;/td&gt;
&lt;td&gt;Perceived only, same total time&lt;/td&gt;
&lt;td&gt;More complex client code; partial output can mislead if it errors mid-stream&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Pick levers by what you're optimizing, cost, latency, or both, and budget for the trade-off.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; Reach for the levers that buy &lt;strong&gt;both&lt;/strong&gt; axes first, caching, routing, trimming, a smaller model. Streaming and batching are special-purpose: streaming fixes &lt;em&gt;perceived&lt;/em&gt; latency for interactive apps; batching trades latency for cost in offline pipelines.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  A minimal router and token-budget trimmer
&lt;/h2&gt;

&lt;p&gt;Here is a stripped-down version of the funnel as code: a token-budget trimmer that keeps context under a hard cap, and a router that tries a small model first and escalates only when the answer looks weak. The point is the &lt;em&gt;shape&lt;/em&gt;, swap in your own client, models, and validation.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;router.py&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tiktoken&lt;/span&gt;

&lt;span class="n"&gt;enc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tiktoken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_encoding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cl100k_base&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;token_len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&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="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;trim_to_budget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;budget&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Keep the most relevant chunks first; drop the rest once the budget is spent.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;kept&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;used&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# chunks arrive pre-sorted by relevance
&lt;/span&gt;        &lt;span class="n"&gt;cost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;token_len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&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;used&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;cost&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;budget&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;  &lt;span class="c1"&gt;# the cheapest token is the one you never send
&lt;/span&gt;        &lt;span class="n"&gt;kept&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;used&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;cost&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;kept&lt;/span&gt;

&lt;span class="n"&gt;SMALL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LARGE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;small-fast-model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;large-strong-model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;looks_weak&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Cheap heuristics to decide if we must escalate.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;i&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;m not sure&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;i cannot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;as an ai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&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;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context_chunks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&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="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;trim_to_budget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context_chunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;budget&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1500&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Context:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;Question: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="c1"&gt;# 1) try the small model first
&lt;/span&gt;    &lt;span class="n"&gt;draft&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="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SMALL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 2) escalate only if the cheap answer is weak
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;looks_weak&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;draft&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;LARGE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&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;draft&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details earn their keep. First, &lt;code&gt;trim_to_budget&lt;/code&gt; enforces a hard input ceiling, without it, a few oversized retrieved chunks silently double your bill on every call. Second, &lt;code&gt;looks_weak&lt;/code&gt; is deliberately crude; in production you'd validate against the task (did it return valid JSON? did it cite a source?) rather than sniffing for phrases. The architecture is identical to what you'd build for an &lt;a href="https://thesimplifiedtech.com/blog/building-ai-agents" rel="noopener noreferrer"&gt;AI agent&lt;/a&gt;: a cheap default path with a deliberate escalation rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes that quietly drain the budget
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Always using the biggest model.&lt;/strong&gt; The flagship is the most expensive and the slowest. Most production traffic is easy and a small model handles it fine, you just never measured the split. Route first; reserve the big model for the requests that earn it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No caching of any kind.&lt;/strong&gt; Real traffic is repetitive: the same questions, the same system prompt, the same retrieved chunks. Without exact, prompt, or semantic caching you re-pay for identical work all day. Caching is usually the single highest-leverage change you can ship.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignoring output tokens.&lt;/strong&gt; Teams obsess over shrinking the prompt and forget that output tokens cost more &lt;em&gt;and&lt;/em&gt; dominate latency. A &lt;code&gt;max_tokens&lt;/code&gt; cap and a prompt that says "answer in two sentences" often saves more than any input trimming.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Letting context grow unbounded.&lt;/strong&gt; Stuffing the whole knowledge base "just in case" inflates every single call. Retrieve and trim to a token budget; relevance beats volume.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimizing latency you can't feel.&lt;/strong&gt; Chasing total generation time when the real fix is &lt;em&gt;perceived&lt;/em&gt; latency. Stream the response and the same model feels twice as fast.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tuning blind.&lt;/strong&gt; Shipping levers without per-request cost and p50/p95 latency dashboards. If you can't see the bill and the tail, you can't tell whether a change helped or hurt.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The whole article in seven lines&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The cheapest, fastest token is the one you never send, that single idea drives every lever.&lt;/li&gt;
&lt;li&gt;You pay in dollars and milliseconds roughly in proportion to input and output tokens.&lt;/li&gt;
&lt;li&gt;Run requests through a funnel: semantic cache, then router, then small model, then large model only on escalation.&lt;/li&gt;
&lt;li&gt;Caching, routing, trimming, and a smaller model save &lt;strong&gt;both&lt;/strong&gt; cost and latency, reach for these first.&lt;/li&gt;
&lt;li&gt;Batching trades latency for cost (offline only); streaming trades nothing but transforms &lt;em&gt;perceived&lt;/em&gt; latency.&lt;/li&gt;
&lt;li&gt;Output tokens cost more and dominate latency, cap them and ask for shorter answers.&lt;/li&gt;
&lt;li&gt;You cannot optimize what you cannot see: instrument per-request cost and p50/p95 before tuning.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;Cost and latency optimization is the final discipline that turns an LLM demo into a shippable product. The strongest next step is to wire a real dashboard, per-request cost, cache hit rate, and the small-vs-large routing split, so every lever you add is measured, not guessed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Solidify the fundamentals these levers rest on with &lt;a href="https://thesimplifiedtech.com/blog/how-llms-actually-work" rel="noopener noreferrer"&gt;How LLMs Actually Work&lt;/a&gt;, why tokens cost what they cost.&lt;/li&gt;
&lt;li&gt;See the routing-and-escalation pattern in a fuller system in &lt;a href="https://thesimplifiedtech.com/blog/building-ai-agents" rel="noopener noreferrer"&gt;Building AI Agents&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Put it all together along the &lt;a href="https://thesimplifiedtech.com/career-paths/ai-engineer" rel="noopener noreferrer"&gt;AI Engineer career path&lt;/a&gt;, where cost and latency sit alongside evaluation and reliability.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://thesimplifiedtech.com/blog/llm-cost-and-latency-optimization" rel="noopener noreferrer"&gt;TheSimplifiedTech&lt;/a&gt;, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>llm</category>
      <category>cost</category>
    </item>
    <item>
      <title>Building AI Agents: From One LLM Call to a Reasoning Loop</title>
      <dc:creator>Sri Balaji</dc:creator>
      <pubDate>Sun, 13 Sep 2026 14:44:43 +0000</pubDate>
      <link>https://dev.to/sri2614/building-ai-agents-from-one-llm-call-to-a-reasoning-loop-idg</link>
      <guid>https://dev.to/sri2614/building-ai-agents-from-one-llm-call-to-a-reasoning-loop-idg</guid>
      <description>&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One call answers. An agent finishes the job.&lt;/li&gt;
&lt;li&gt;What an agent actually is&lt;/li&gt;
&lt;li&gt;The agent loop, drawn out&lt;/li&gt;
&lt;li&gt;One call vs. chain vs. agent, pick the smallest thing that works&lt;/li&gt;
&lt;li&gt;A minimal agent loop you can read&lt;/li&gt;
&lt;li&gt;Planning, memory, and context&lt;/li&gt;
&lt;li&gt;Guardrails: tools are the attack surface&lt;/li&gt;
&lt;li&gt;Failure modes that bite in production&lt;/li&gt;
&lt;li&gt;Takeaways&lt;/li&gt;
&lt;li&gt;Where to go next&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  One call answers. An agent finishes the job.
&lt;/h2&gt;

&lt;p&gt;You wire up an LLM, send a prompt, get a paragraph back. It's impressive, until the task needs more than one shot. "What's our current AWS bill, and which service jumped this month?" A single call can't answer that. It has no live data, it can't run a query, and it can't check its own math. So it guesses, confidently, and you ship a wrong number.&lt;/p&gt;

&lt;p&gt;The fix isn't a smarter prompt. It's a different shape. Instead of one call that must know everything, you give the model &lt;strong&gt;tools&lt;/strong&gt; and let it &lt;strong&gt;loop&lt;/strong&gt;: think about what it needs, call a tool to get it, look at the result, and decide whether it's done. That loop is what people mean by an &lt;strong&gt;agent&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;Who this is for:&lt;/strong&gt; Engineers who've made a few LLM calls and now want the model to &lt;em&gt;do&lt;/em&gt; things, query a database, hit an API, read a file, and self-correct along the way. You should be comfortable with &lt;a href="https://thesimplifiedtech.com/blog/structured-output-and-tool-calling" rel="noopener noreferrer"&gt;structured output and tool calling&lt;/a&gt; and have a rough feel for &lt;a href="https://thesimplifiedtech.com/blog/llm-cost-and-latency-optimization" rel="noopener noreferrer"&gt;LLM cost and latency&lt;/a&gt;. No agent framework required; we build the loop by hand so you can see every moving part.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What an agent actually is
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;An agent is an LLM running in a loop: it reasons about a goal, picks a tool, executes it, observes the result, and repeats, until it decides the goal is met or it hits a limit you set.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The pattern has a name: &lt;strong&gt;ReAct&lt;/strong&gt;, &lt;em&gt;reason → act → observe → repeat&lt;/em&gt;. The model reasons in plain language about what to do next, acts by emitting a tool call, then observes the tool's output, which gets fed back into the next turn. Nothing magical: it's a &lt;code&gt;while&lt;/code&gt; loop where the LLM is the decision-maker and tools are the hands.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;In the real world&lt;/th&gt;
&lt;th&gt;In tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;You hand an intern a goal, not step-by-step instructions&lt;/td&gt;
&lt;td&gt;You give the agent a task and a system prompt, not a fixed script&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The intern looks things up, runs the report, calls the API&lt;/td&gt;
&lt;td&gt;The agent calls tools, search, SQL, HTTP, a calculator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;They read the result and decide what to do next&lt;/td&gt;
&lt;td&gt;Tool output is fed back; the model reasons about the next step&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;They stop when the task is done, or ask when stuck&lt;/td&gt;
&lt;td&gt;The loop ends on a 'final answer' or a step / cost limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A bad intern loops forever re-checking the same thing&lt;/td&gt;
&lt;td&gt;An unguarded agent loops, re-calls tools, and burns tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;An agent is an intern who can use tools and check their own work.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The agent loop, drawn out
&lt;/h2&gt;

&lt;p&gt;Every agent, no matter the framework, is this cycle. The model never touches the outside world directly; it asks for a tool, your code runs the tool, and the result comes back as the next observation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fZ29hbCgiR29hbDxici8-dXNlciB0YXNrICsgc3lzdGVtIHByb21wdCIpCiAgY2xhc3Mgbl9nb2FsIGNsaWVudDsKICBuX3JlYXNvbigiUmVhc29uPGJyLz53aGF0IGRvIEkgbmVlZCBuZXh0PyIpCiAgY2xhc3Mgbl9yZWFzb24gY29tcHV0ZTsKICBuX2Nob29zZSgiQ2hvb3NlIHRvb2w8YnIvPm5hbWUgKyBhcmd1bWVudHMiKQogIGNsYXNzIG5fY2hvb3NlIGNvbXB1dGU7CiAgbl9leGVjdXRlKCJFeGVjdXRlPGJyLz55b3VyIGNvZGUgcnVucyB0aGUgdG9vbCIpCiAgY2xhc3Mgbl9leGVjdXRlIGV4dGVybmFsOwogIG5fb2JzZXJ2ZSgiT2JzZXJ2ZTxici8-dG9vbCByZXN1bHQgYmFjayB0byBtb2RlbCIpCiAgY2xhc3Mgbl9vYnNlcnZlIGRhdGE7CiAgbl9kb25lKCJEb25lPzxici8-YW5zd2VyIG9yIHN0ZXAgY2FwIikKICBjbGFzcyBuX2RvbmUgcXVldWU7CiAgbl9maW5hbCgiRmluYWwgYW5zd2VyPGJyLz5yZXR1cm4gdG8gdXNlciIpCiAgY2xhc3Mgbl9maW5hbCBjbGllbnQ7CiAgbl9nb2FsIC0tPiBuX3JlYXNvbgogIG5fcmVhc29uIC0tPiBuX2Nob29zZQogIG5fY2hvb3NlIC0tPiBuX2V4ZWN1dGUKICBuX2V4ZWN1dGUgLS0-IG5fb2JzZXJ2ZQogIG5fb2JzZXJ2ZSAtLi0-fCJmZWVkIGJhY2sifCBuX2RvbmUKICBuX2RvbmUgLS0-fCJub3QgeWV0LCBsb29wInwgbl9yZWFzb24KICBuX2RvbmUgLS0-fCJ5ZXMifCBuX2ZpbmFs%3FbgColor%3D0d1017%26type%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fZ29hbCgiR29hbDxici8-dXNlciB0YXNrICsgc3lzdGVtIHByb21wdCIpCiAgY2xhc3Mgbl9nb2FsIGNsaWVudDsKICBuX3JlYXNvbigiUmVhc29uPGJyLz53aGF0IGRvIEkgbmVlZCBuZXh0PyIpCiAgY2xhc3Mgbl9yZWFzb24gY29tcHV0ZTsKICBuX2Nob29zZSgiQ2hvb3NlIHRvb2w8YnIvPm5hbWUgKyBhcmd1bWVudHMiKQogIGNsYXNzIG5fY2hvb3NlIGNvbXB1dGU7CiAgbl9leGVjdXRlKCJFeGVjdXRlPGJyLz55b3VyIGNvZGUgcnVucyB0aGUgdG9vbCIpCiAgY2xhc3Mgbl9leGVjdXRlIGV4dGVybmFsOwogIG5fb2JzZXJ2ZSgiT2JzZXJ2ZTxici8-dG9vbCByZXN1bHQgYmFjayB0byBtb2RlbCIpCiAgY2xhc3Mgbl9vYnNlcnZlIGRhdGE7CiAgbl9kb25lKCJEb25lPzxici8-YW5zd2VyIG9yIHN0ZXAgY2FwIikKICBjbGFzcyBuX2RvbmUgcXVldWU7CiAgbl9maW5hbCgiRmluYWwgYW5zd2VyPGJyLz5yZXR1cm4gdG8gdXNlciIpCiAgY2xhc3Mgbl9maW5hbCBjbGllbnQ7CiAgbl9nb2FsIC0tPiBuX3JlYXNvbgogIG5fcmVhc29uIC0tPiBuX2Nob29zZQogIG5fY2hvb3NlIC0tPiBuX2V4ZWN1dGUKICBuX2V4ZWN1dGUgLS0-IG5fb2JzZXJ2ZQogIG5fb2JzZXJ2ZSAtLi0-fCJmZWVkIGJhY2sifCBuX2RvbmUKICBuX2RvbmUgLS0-fCJub3QgeWV0LCBsb29wInwgbl9yZWFzb24KICBuX2RvbmUgLS0-fCJ5ZXMifCBuX2ZpbmFs%3FbgColor%3D0d1017%26type%3Dpng" alt="The ReAct loop: reason, act, observe, repeat, with a guard that forces an exit." width="1658" height="156"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The ReAct loop: reason, act, observe, repeat, with a guard that forces an exit.&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Start with a goal&lt;/strong&gt;: The user's task plus a system prompt describing the role and the tools available. This is the only fixed input.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reason&lt;/strong&gt;: The model thinks about what it needs next, in plain language. "To find the cost spike I need this month's bill broken down by service."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choose a tool&lt;/strong&gt;: It emits a structured tool call: a name (&lt;code&gt;query_billing&lt;/code&gt;) and arguments (&lt;code&gt;{ month: "2026-06" }&lt;/code&gt;). This is just structured output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execute&lt;/strong&gt;: Your code, not the model, runs the tool, hits the API or DB, and captures the result. The model is sandboxed; it only asks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observe&lt;/strong&gt;: The tool's output is appended to the conversation as an observation and handed back to the model for the next turn.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Done?&lt;/strong&gt;: The model decides: is the goal met? If yes, it returns a final answer. If no, the loop repeats, bounded by a max-step guard so it can never run forever.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  One call vs. chain vs. agent, pick the smallest thing that works
&lt;/h2&gt;

&lt;p&gt;An agent is the most powerful shape and the most expensive, slowest, and least predictable. Reach for it only when the &lt;em&gt;number of steps isn't known up front&lt;/em&gt;. If you can hardcode the steps, don't make the model decide them.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Shape&lt;/th&gt;
&lt;th&gt;How it works&lt;/th&gt;
&lt;th&gt;Use when&lt;/th&gt;
&lt;th&gt;Cost &amp;amp; risk&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Single LLM call&lt;/td&gt;
&lt;td&gt;One prompt, one response. No tools, no loop.&lt;/td&gt;
&lt;td&gt;The model already knows the answer or it's pure text-in / text-out (summarize, classify, rewrite).&lt;/td&gt;
&lt;td&gt;Cheapest, fastest, most predictable. Bounded by one call.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chain&lt;/td&gt;
&lt;td&gt;A fixed sequence of calls / steps you wire yourself (e.g. retrieve → answer).&lt;/td&gt;
&lt;td&gt;You know the steps in advance and the order never changes. RAG is a chain.&lt;/td&gt;
&lt;td&gt;Predictable cost (N steps). You control the flow, not the model.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent&lt;/td&gt;
&lt;td&gt;The model decides which tools to call and how many times, in a loop.&lt;/td&gt;
&lt;td&gt;The path depends on intermediate results, unknown number of steps, branching, self-correction.&lt;/td&gt;
&lt;td&gt;Variable cost, can loop, hardest to debug. Needs guardrails.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Match the shape to the task. Default to the simplest one that fits.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;The honest default:&lt;/strong&gt; Most "agent" projects are really a chain in disguise. If you can draw the steps on a whiteboard and they never branch, build a chain, it's cheaper, faster, and you can actually test it. Save the agent for the genuinely open-ended cases.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  A minimal agent loop you can read
&lt;/h2&gt;

&lt;p&gt;Here's the whole idea in ~50 lines: one tool, the loop, and a hard step cap. No framework. The model gets a tool definition, and on each turn it either calls the tool or returns a final answer. The &lt;code&gt;MAX_STEPS&lt;/code&gt; guard is the single most important line, it's the difference between a bounded agent and a runaway bill.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;agent.py&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;anthropic&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;anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;MAX_STEPS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;  &lt;span class="c1"&gt;# hard guard: the loop can NEVER run longer than this
&lt;/span&gt;
&lt;span class="c1"&gt;# One tool the model is allowed to use.
&lt;/span&gt;&lt;span class="n"&gt;TOOLS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query_billing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Return this month&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s cloud spend, broken down by service.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input_schema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;month&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YYYY-MM&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;month&lt;/span&gt;&lt;span class="sh"&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# YOUR code runs the tool, the model only asked for it.
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query_billing&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EC2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;S3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RDS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NAT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;950&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown tool: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;task&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;step&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MAX_STEPS&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;resp&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;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-4-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a FinOps assistant. Use tools to get real numbers; never guess.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TOOLS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# No tool call -&amp;gt; the model is done. Return its answer.
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stop_reason&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_use&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Otherwise: execute every requested tool, feed results back, loop.
&lt;/span&gt;        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assistant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&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;block&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&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;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_use&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_use_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="c1"&gt;# Hit the cap without finishing, fail loud, don't loop forever.
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Stopped: hit MAX_STEPS without a final answer.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Which service drove our cost up this month, and by how much?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the control flow, not the API. The model reasons, asks for &lt;code&gt;query_billing&lt;/code&gt;, your &lt;code&gt;run_tool&lt;/code&gt; executes it, the result goes back, and the model reasons again, this time with real numbers, and returns the answer. The &lt;code&gt;for step in range(MAX_STEPS)&lt;/code&gt; is the loop &lt;em&gt;and&lt;/em&gt; the guard in one line. Everything an agent framework adds is built on exactly this skeleton.&lt;/p&gt;

&lt;h2&gt;
  
  
  Planning, memory, and context
&lt;/h2&gt;

&lt;p&gt;Three things separate a toy loop from something that survives real tasks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Planning
&lt;/h3&gt;

&lt;p&gt;For multi-step tasks, ask the model to lay out a plan &lt;em&gt;first&lt;/em&gt;, then execute step by step. A plan in the context window keeps the agent on-rails and gives it something to check progress against, instead of re-deciding the whole approach on every turn (which is how agents drift and loop).&lt;/p&gt;

&lt;h3&gt;
  
  
  Memory and context
&lt;/h3&gt;

&lt;p&gt;The model has no memory between turns except what's in the message list. Every tool result you append is context the model now "remembers", and context isn't free. Each turn re-sends the entire growing history, so a 10-step agent pays for the conversation 10 times over. The practical levers: &lt;strong&gt;summarize&lt;/strong&gt; old tool results once they're no longer needed verbatim, &lt;strong&gt;trim&lt;/strong&gt; giant payloads before appending them, and keep the system prompt tight. This is the same cost math from &lt;a href="https://thesimplifiedtech.com/blog/llm-cost-and-latency-optimization" rel="noopener noreferrer"&gt;LLM cost and latency optimization&lt;/a&gt;, just amplified by the loop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stopping conditions
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Natural stop&lt;/strong&gt;, the model returns a final answer with no tool call (&lt;code&gt;stop_reason != "tool_use"&lt;/code&gt;). The good case.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step cap&lt;/strong&gt;, &lt;code&gt;MAX_STEPS&lt;/code&gt; hit. Non-negotiable; it's your circuit breaker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Budget cap&lt;/strong&gt;, track cumulative tokens / dollars and bail when you cross a threshold, independent of step count.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wall-clock timeout&lt;/strong&gt;, for user-facing agents, cap total latency so a slow tool can't hang the request forever.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Guardrails: tools are the attack surface
&lt;/h2&gt;

&lt;p&gt;The model decides &lt;em&gt;what&lt;/em&gt; to call; your code decides &lt;em&gt;whether to allow it&lt;/em&gt;. Never trust tool arguments blindly, the model can hallucinate a &lt;code&gt;DROP TABLE&lt;/code&gt; just as easily as a &lt;code&gt;SELECT&lt;/code&gt;. Treat every tool call as untrusted input.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Validate arguments&lt;/strong&gt; against the schema and your own rules before executing, reject anything out of bounds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope tool permissions&lt;/strong&gt;, give a support agent read-only DB access, not write. The blast radius of a bad call is whatever the tool can do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make destructive tools confirm&lt;/strong&gt;, require a human approval step for anything that deletes, pays, or emails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Return errors as observations&lt;/strong&gt;, not exceptions, when a tool fails, feed the error back so the model can recover instead of crashing the loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log every reason → tool → result triple&lt;/strong&gt;, when an agent does something weird, this trace is the only way you'll understand why.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Failure modes that bite in production
&lt;/h2&gt;

&lt;p&gt;Agents fail in ways single calls never do, because the loop compounds mistakes. The four below cause most of the pain.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Infinite / stuck loops.&lt;/strong&gt; The model keeps re-calling the same tool with the same args, never converging, often because the tool result didn't actually answer its question. Without a step cap, this runs until you notice the bill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No step cap.&lt;/strong&gt; The single most common omission. One off-by-one in the model's reasoning and you've got an unbounded &lt;code&gt;while True&lt;/code&gt;. Always set &lt;code&gt;MAX_STEPS&lt;/code&gt; and fail loud when you hit it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost blowups.&lt;/strong&gt; Every turn re-sends the full history, so cost grows roughly quadratically with step count. A chatty agent that takes 12 turns can cost 20× a single call. Track cumulative tokens, summarize old context, and cap the budget.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bad tool use.&lt;/strong&gt; The model calls the wrong tool, passes malformed args, or invents a tool that doesn't exist. Clear tool descriptions, strict schemas, and good error-as-observation handling fix most of it, but expect it and test for it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Over-agenting.&lt;/strong&gt; Using an agent where a chain or a single call would do. You inherit all the failure modes above for a task whose steps you already knew. The cure is upstream: pick the right shape.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The whole article in seven lines&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An agent is an LLM in a loop: &lt;strong&gt;reason → act → observe → repeat&lt;/strong&gt; (ReAct) until done.&lt;/li&gt;
&lt;li&gt;The model only &lt;em&gt;asks&lt;/em&gt; for tools; &lt;strong&gt;your code executes them&lt;/strong&gt; and feeds results back as observations.&lt;/li&gt;
&lt;li&gt;Pick the smallest shape: &lt;strong&gt;single call&lt;/strong&gt; if it knows the answer, &lt;strong&gt;chain&lt;/strong&gt; if the steps are fixed, &lt;strong&gt;agent&lt;/strong&gt; only when the path is unknown.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;max-step guard&lt;/strong&gt; is mandatory, it's the line between a bounded agent and a runaway bill.&lt;/li&gt;
&lt;li&gt;Context isn't free: every turn re-sends the history, so &lt;strong&gt;summarize and trim&lt;/strong&gt; as the loop grows.&lt;/li&gt;
&lt;li&gt;Tools are the attack surface, &lt;strong&gt;validate args, scope permissions, confirm destructive actions.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;The classic failures are &lt;strong&gt;infinite loops, no step cap, cost blowups, bad tool use, and over-agenting&lt;/strong&gt;, design against all five.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;Build the minimal loop above with one real tool against an API you control, then add a step cap and a token counter before you add a second tool. You'll learn more from one hand-built agent than from any framework tutorial.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tighten the foundation: &lt;a href="https://thesimplifiedtech.com/blog/structured-output-and-tool-calling" rel="noopener noreferrer"&gt;Structured Output &amp;amp; Tool Calling&lt;/a&gt;, tool calls are just structured output, so reliability there is reliability here.&lt;/li&gt;
&lt;li&gt;Control the loop's economics: &lt;a href="https://thesimplifiedtech.com/blog/llm-cost-and-latency-optimization" rel="noopener noreferrer"&gt;LLM Cost &amp;amp; Latency Optimization&lt;/a&gt;, the techniques matter most once every turn re-sends the context.&lt;/li&gt;
&lt;li&gt;See where agents fit in the broader role: the &lt;a href="https://thesimplifiedtech.com/career-paths/ai-engineer" rel="noopener noreferrer"&gt;AI Engineer career path&lt;/a&gt; walks the path from prompting to retrieval to agentic systems.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://thesimplifiedtech.com/blog/building-ai-agents" rel="noopener noreferrer"&gt;TheSimplifiedTech&lt;/a&gt;, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>llm</category>
      <category>agents</category>
    </item>
    <item>
      <title>Evaluating LLM Applications</title>
      <dc:creator>Sri Balaji</dc:creator>
      <pubDate>Sat, 12 Sep 2026 13:48:42 +0000</pubDate>
      <link>https://dev.to/sri2614/evaluating-llm-applications-56m3</link>
      <guid>https://dev.to/sri2614/evaluating-llm-applications-56m3</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚡ &lt;strong&gt;TL;DR:&lt;/strong&gt; Treat evals as unit tests for non-deterministic output. Build golden datasets, deterministic checks, and LLM-as-judge scoring, then add regression gates so you ship on evidence instead of vibes. Run both offline and online.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You changed a prompt and have no idea if it got better&lt;/li&gt;
&lt;li&gt;The principle: evals are unit tests for non-deterministic output&lt;/li&gt;
&lt;li&gt;The shape of an eval pipeline&lt;/li&gt;
&lt;li&gt;Choosing a scoring method&lt;/li&gt;
&lt;li&gt;A tiny eval harness&lt;/li&gt;
&lt;li&gt;Offline vs online: you need both&lt;/li&gt;
&lt;li&gt;Common mistakes that cost hours&lt;/li&gt;
&lt;li&gt;Takeaways&lt;/li&gt;
&lt;li&gt;Where to go next&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  You changed a prompt and have no idea if it got better
&lt;/h2&gt;

&lt;p&gt;Here is the most common moment in LLM development. You tweak a system prompt, add a line, reword an instruction, swap a model. You run it on the one example you happen to have open, the output looks nicer, and you ship it. Then a week later a user complains the bot got &lt;em&gt;worse&lt;/em&gt; at something you never thought to test, and you have no way to prove whether your change helped, hurt, or did nothing at all.&lt;/p&gt;

&lt;p&gt;This is &lt;strong&gt;vibes-based shipping&lt;/strong&gt;, and almost everyone does it at first. The reason is simple: with normal code you write a test, it passes or fails, done. With an LLM the output is different every run, there is no single "correct" answer, and "better" is a judgment call. So people skip evaluation entirely, which makes it the hardest and most-skipped part of building real AI products.&lt;/p&gt;

&lt;p&gt;This article is the antidote. You will build a tiny eval harness that scores your prompt's outputs against a fixed set of examples, so the next time you change something you get a &lt;em&gt;number&lt;/em&gt;, not a feeling.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;Who this is for:&lt;/strong&gt; Anyone who has wired up an LLM call and now wants to change it &lt;em&gt;safely&lt;/em&gt;, app developers, prompt engineers, and AI engineers. You should be comfortable reading Python and have made at least one API call to a model. No ML background needed. If you have not written a prompt yet, start with &lt;a href="https://thesimplifiedtech.com/blog/prompt-engineering-fundamentals" rel="noopener noreferrer"&gt;Prompt Engineering Fundamentals&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The principle: evals are unit tests for non-deterministic output
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;If you cannot measure whether a change made your LLM app better, you are not engineering, you are gambling with extra steps.&lt;/p&gt;

&lt;p&gt;The eval mindset&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The mental model that unlocks everything: &lt;strong&gt;an eval is a unit test for a system whose output you cannot predict exactly.&lt;/strong&gt; A normal unit test asserts &lt;code&gt;add(2, 2) === 4&lt;/code&gt;. You can't do that with an LLM, the same input yields different wording every time. So instead of asserting &lt;em&gt;exact equality&lt;/em&gt;, an eval asserts &lt;em&gt;a property holds&lt;/em&gt;: the answer contains the right account ID, the JSON parses, the tone is professional, the summary doesn't invent facts.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;In the real world&lt;/th&gt;
&lt;th&gt;In tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unit test asserting exact output&lt;/td&gt;
&lt;td&gt;Exact-match scorer (when there IS one right answer)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test asserting a property (sorted, non-empty)&lt;/td&gt;
&lt;td&gt;Heuristic scorer (regex, JSON-parses, length, contains-keyword)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code reviewer judging readability&lt;/td&gt;
&lt;td&gt;LLM-as-judge scoring tone, relevance, faithfulness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test suite you run before every merge&lt;/td&gt;
&lt;td&gt;Eval suite you run before every prompt change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI failing the build on a regression&lt;/td&gt;
&lt;td&gt;Regression gate failing the deploy on a score drop&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Same instinct as classic testing, just adapted for output you can't pin down exactly.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of an eval pipeline
&lt;/h2&gt;

&lt;p&gt;Before writing code, picture the flow. You start with a &lt;strong&gt;golden set&lt;/strong&gt;, fixed inputs paired with what a good answer looks like. Each input runs through your prompt, the output goes to one or more &lt;strong&gt;scorers&lt;/strong&gt;, and the scores roll up into an aggregate that a &lt;strong&gt;regression gate&lt;/strong&gt; compares against your last known-good run.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fZ29sZGVuKCJHb2xkZW4gc2V0PGJyLz5pbnB1dHMgKyBleHBlY3RlZCIpCiAgY2xhc3Mgbl9nb2xkZW4gZGF0YTsKICBuX3J1bigiUnVuIHByb21wdDxici8-TExNIHVuZGVyIHRlc3QiKQogIGNsYXNzIG5fcnVuIGNvbXB1dGU7CiAgbl9leGFjdCgiRXhhY3QgLyBoZXVyaXN0aWM8YnIvPmRldGVybWluaXN0aWMgc2NvcmVycyIpCiAgY2xhc3Mgbl9leGFjdCBjb21wdXRlOwogIG5fanVkZ2UoIkxMTS1hcy1qdWRnZTxici8-Z3JhZGVkIGJ5IGEgbW9kZWwiKQogIGNsYXNzIG5fanVkZ2UgZXh0ZXJuYWw7CiAgbl9zY29yZSgiQWdncmVnYXRlIHNjb3JlPGJyLz5wYXNzIHJhdGUgcGVyIHNjb3JlciIpCiAgY2xhc3Mgbl9zY29yZSBvYnNlcnZhYmlsaXR5OwogIG5fZ2F0ZSgiUmVncmVzc2lvbiBnYXRlPGJyLz7iiaUgYmFzZWxpbmU_IikKICBjbGFzcyBuX2dhdGUgZWRnZTsKICBuX2dvbGRlbiAtLT58ImVhY2ggY2FzZSJ8IG5fcnVuCiAgbl9ydW4gLS0-IG5fZXhhY3QKICBuX3J1biAtLi0-IG5fanVkZ2UKICBuX2V4YWN0IC0tPiBuX3Njb3JlCiAgbl9qdWRnZSAtLi0-IG5fc2NvcmUKICBuX3Njb3JlIC0tPnwiY29tcGFyZSJ8IG5fZ2F0ZQ%3FbgColor%3D0d1017%26type%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fZ29sZGVuKCJHb2xkZW4gc2V0PGJyLz5pbnB1dHMgKyBleHBlY3RlZCIpCiAgY2xhc3Mgbl9nb2xkZW4gZGF0YTsKICBuX3J1bigiUnVuIHByb21wdDxici8-TExNIHVuZGVyIHRlc3QiKQogIGNsYXNzIG5fcnVuIGNvbXB1dGU7CiAgbl9leGFjdCgiRXhhY3QgLyBoZXVyaXN0aWM8YnIvPmRldGVybWluaXN0aWMgc2NvcmVycyIpCiAgY2xhc3Mgbl9leGFjdCBjb21wdXRlOwogIG5fanVkZ2UoIkxMTS1hcy1qdWRnZTxici8-Z3JhZGVkIGJ5IGEgbW9kZWwiKQogIGNsYXNzIG5fanVkZ2UgZXh0ZXJuYWw7CiAgbl9zY29yZSgiQWdncmVnYXRlIHNjb3JlPGJyLz5wYXNzIHJhdGUgcGVyIHNjb3JlciIpCiAgY2xhc3Mgbl9zY29yZSBvYnNlcnZhYmlsaXR5OwogIG5fZ2F0ZSgiUmVncmVzc2lvbiBnYXRlPGJyLz7iiaUgYmFzZWxpbmU_IikKICBjbGFzcyBuX2dhdGUgZWRnZTsKICBuX2dvbGRlbiAtLT58ImVhY2ggY2FzZSJ8IG5fcnVuCiAgbl9ydW4gLS0-IG5fZXhhY3QKICBuX3J1biAtLi0-IG5fanVkZ2UKICBuX2V4YWN0IC0tPiBuX3Njb3JlCiAgbl9qdWRnZSAtLi0-IG5fc2NvcmUKICBuX3Njb3JlIC0tPnwiY29tcGFyZSJ8IG5fZ2F0ZQ%3FbgColor%3D0d1017%26type%3Dpng" alt="An eval pipeline: golden set → run prompt → layered scorers → aggregate score → regression gate." width="1132" height="230"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;An eval pipeline: golden set → run prompt → layered scorers → aggregate score → regression gate.&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Collect a golden set&lt;/strong&gt;: Gather 20-50 real inputs your app will actually see. Pull them from logs, support tickets, or your own head. For each, write down what a good output must contain, not the exact wording, just the must-haves.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run the prompt over every case&lt;/strong&gt;: Loop through the golden set, call the model with your current prompt for each input, and capture the raw output. Pin temperature low (or 0) so runs are comparable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Score each output&lt;/strong&gt;: Send every output through your scorers. Start with cheap deterministic checks (does it contain the order ID? does the JSON parse?), then add an LLM-judge for the fuzzy stuff (is the tone right? is it faithful to the source?).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aggregate and gate&lt;/strong&gt;: Average the scores into a pass rate per scorer. Compare against your last saved baseline. If the new run drops below it, the gate fails and you investigate before shipping.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Choosing a scoring method
&lt;/h2&gt;

&lt;p&gt;There is no single "the" eval method, you layer several, cheapest first. Run deterministic checks on every case because they cost nothing, and reserve expensive methods (model judges, humans) for the questions cheap checks can't answer.&lt;/p&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;What it measures&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Signal quality&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Exact match&lt;/td&gt;
&lt;td&gt;Output equals expected string (classification labels, structured IDs)&lt;/td&gt;
&lt;td&gt;Near zero&lt;/td&gt;
&lt;td&gt;Perfect, but only where one right answer exists&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Heuristic&lt;/td&gt;
&lt;td&gt;Properties: regex, JSON-parses, contains keyword, length, no banned words&lt;/td&gt;
&lt;td&gt;Near zero&lt;/td&gt;
&lt;td&gt;Good for structure/format; blind to meaning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM-as-judge&lt;/td&gt;
&lt;td&gt;Fuzzy qualities: relevance, tone, faithfulness, helpfulness&lt;/td&gt;
&lt;td&gt;$ per case (extra model call)&lt;/td&gt;
&lt;td&gt;Strong if the judge prompt is calibrated; biased if not&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Human review&lt;/td&gt;
&lt;td&gt;Anything subtle a model still gets wrong; ground truth&lt;/td&gt;
&lt;td&gt;$$$ + slow&lt;/td&gt;
&lt;td&gt;Highest, but doesn't scale; use to calibrate the others&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Eval methods, cheapest to most expensive. Layer them, don't pick just one.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Always start deterministic:&lt;/strong&gt; A surprising amount of "quality" is just structure: valid JSON, the right field present, no apology when none is needed. Heuristic scorers catch these for free and run in milliseconds. Only reach for an LLM-judge once the deterministic checks pass and you still need to grade meaning.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  A tiny eval harness
&lt;/h2&gt;

&lt;p&gt;Here is the whole idea in about 60 lines of Python: a golden set, two scorers (one heuristic, one LLM-judge), and an aggregate with a regression gate. The model call is stubbed behind &lt;code&gt;run_prompt&lt;/code&gt; and &lt;code&gt;judge&lt;/code&gt; so you can drop in whatever provider you use.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;eval_harness.py&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;mean&lt;/span&gt;

&lt;span class="c1"&gt;# 1. The golden set: real inputs + what a good answer must contain.
&lt;/span&gt;&lt;span class="n"&gt;GOLDEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s the refund window?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;must_include&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;30 days&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Do you ship to Germany?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;must_include&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Is my data sold to third parties?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;must_include&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# Your app's prompt under test. Swap in a real API call.
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;system&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a concise support agent. Answer in one sentence.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;call_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="p"&gt;,&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;user_input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# --- Scorers: each returns a float in [0, 1] ---
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;heuristic_scorer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Deterministic: does the answer contain the required fact?
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;must_include&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;

&lt;span class="n"&gt;JUDGE_PROMPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Score the ANSWER from 0-1 for how clearly and politely it &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resolves the QUESTION. Reply with ONLY the number.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;QUESTION: {q}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;ANSWER: {a}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;llm_judge_scorer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;judge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JUDGE_PROMPT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&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;output&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;())))&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;  &lt;span class="c1"&gt;# un-parseable judge output = fail loudly
&lt;/span&gt;
&lt;span class="n"&gt;SCORERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;heuristic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;heuristic_scorer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;judge&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;llm_judge_scorer&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;          &lt;span class="c1"&gt;# scorer name -&amp;gt; mean score
&lt;/span&gt;    &lt;span class="n"&gt;overall&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;per_scorer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&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;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SCORERS&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;case&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;GOLDEN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;"&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;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SCORERS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;per_scorer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vals&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;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vals&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;per_scorer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;overall&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;

&lt;span class="c1"&gt;# --- Regression gate: fail if we dropped below the saved baseline ---
&lt;/span&gt;&lt;span class="n"&gt;BASELINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;  &lt;span class="c1"&gt;# load this from your last green run, don't hardcode forever
&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;per-scorer:&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;overall:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;round&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="n"&gt;overall&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;overall&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;BASELINE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REGRESSION: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;overall&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &amp;lt; baseline &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASELINE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PASS, safe to ship.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this before and after a prompt change. The same three cases, the same scorers, two numbers you can compare. That single &lt;code&gt;assert&lt;/code&gt; is your regression gate, wire it into CI and a prompt edit that quietly breaks an answer can no longer reach production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Offline vs online: you need both
&lt;/h2&gt;

&lt;p&gt;Everything above is &lt;strong&gt;offline eval&lt;/strong&gt;, a fixed golden set you run on demand. It is fast, repeatable, and perfect for catching regressions before you ship. But a golden set only contains the inputs you &lt;em&gt;thought of&lt;/em&gt;. Real users will phrase things you never imagined.&lt;/p&gt;

&lt;p&gt;That's where &lt;strong&gt;online eval&lt;/strong&gt; comes in: scoring a sample of &lt;em&gt;live production traffic&lt;/em&gt;. You log real inputs and outputs, run the same scorers (plus lightweight signals like thumbs-up/down, did-the-user-retry, did-they-escalate-to-a-human), and watch the trend. Online eval tells you what offline can't, how the app behaves on the long tail of real questions. The two feed each other: failures you spot online become new cases in your golden set, which makes your offline gate smarter over time. Wiring this into production logging and dashboards is the subject of &lt;a href="https://thesimplifiedtech.com/blog/llmops-productionizing-llm-apps" rel="noopener noreferrer"&gt;LLMOps: Productionizing LLM Apps&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes that cost hours
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No dataset at all.&lt;/strong&gt; Eyeballing one example is not evaluation. Without a fixed golden set every "improvement" is anecdote. Twenty real cases beat zero, start there, grow it as you find failures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Judge bias.&lt;/strong&gt; An LLM-judge inherits the quirks of its model: it favors longer answers, rewards confident tone over correctness, and rates its &lt;em&gt;own&lt;/em&gt; outputs higher than a rival model's. Calibrate the judge against human labels on a handful of cases before you trust it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only offline.&lt;/strong&gt; A green offline suite feels safe, but it only tests inputs you imagined. Skipping online eval means real-world failures stay invisible until a user reports them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scoring with the model you're testing.&lt;/strong&gt; Using the same model as both the app &lt;em&gt;and&lt;/em&gt; the judge bakes in shared blind spots. Use a different (often stronger) model to judge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A vague golden answer.&lt;/strong&gt; "A good response" is unscorable. Write the &lt;em&gt;must-haves&lt;/em&gt;, the fact, the format, the forbidden phrases, so a scorer (or a human) can decide objectively.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Letting the baseline rot.&lt;/strong&gt; A regression gate is only as honest as its baseline. Update it from your last green run, and never lower it to make a failing change pass.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The whole article in seven lines&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Evals are unit tests for non-deterministic output, assert &lt;em&gt;properties&lt;/em&gt;, not exact equality.&lt;/li&gt;
&lt;li&gt;Build a golden set first: 20-50 real inputs + the must-haves of a good answer.&lt;/li&gt;
&lt;li&gt;Layer scorers cheapest-first: exact match → heuristic → LLM-as-judge → human review.&lt;/li&gt;
&lt;li&gt;Deterministic checks are free and catch most structure bugs, always run them.&lt;/li&gt;
&lt;li&gt;A regression gate (new score ≥ baseline) is what kills vibes-based shipping.&lt;/li&gt;
&lt;li&gt;Offline eval catches regressions; online eval catches the long tail. You need both.&lt;/li&gt;
&lt;li&gt;Watch for judge bias, calibrate against humans and never let the model grade itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;Evaluation is the discipline that turns prompt-tweaking into engineering. The next step is putting the harness somewhere it runs automatically and watching real traffic.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sharpen the thing you're evaluating: &lt;a href="https://thesimplifiedtech.com/blog/prompt-engineering-fundamentals" rel="noopener noreferrer"&gt;Prompt Engineering Fundamentals&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Run evals in CI and score live traffic: &lt;a href="https://thesimplifiedtech.com/blog/llmops-productionizing-llm-apps" rel="noopener noreferrer"&gt;LLMOps: Productionizing LLM Apps&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;See where eval fits in the broader role: the &lt;a href="https://thesimplifiedtech.com/career-paths/ai-engineer" rel="noopener noreferrer"&gt;AI Engineer career path&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://thesimplifiedtech.com/blog/evaluating-llm-applications" rel="noopener noreferrer"&gt;TheSimplifiedTech&lt;/a&gt;, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>llm</category>
      <category>evaluation</category>
    </item>
    <item>
      <title>Structured Output &amp; Tool Calling</title>
      <dc:creator>Sri Balaji</dc:creator>
      <pubDate>Fri, 11 Sep 2026 14:45:10 +0000</pubDate>
      <link>https://dev.to/sri2614/structured-output-tool-calling-4lip</link>
      <guid>https://dev.to/sri2614/structured-output-tool-calling-4lip</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚡ &lt;strong&gt;TL;DR:&lt;/strong&gt; Stop regex-parsing prose. You will learn to force clean JSON against a schema you define and run the &lt;strong&gt;request, execute, return&lt;/strong&gt; loop so the model can safely call your functions, with four levels of reliability to dial in.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The problem with prose&lt;/li&gt;
&lt;li&gt;Two mental models&lt;/li&gt;
&lt;li&gt;The tool-calling loop&lt;/li&gt;
&lt;li&gt;Four levels of reliability&lt;/li&gt;
&lt;li&gt;Code: a tool definition and the loop&lt;/li&gt;
&lt;li&gt;Code: structured output with a schema&lt;/li&gt;
&lt;li&gt;Common mistakes that cost hours&lt;/li&gt;
&lt;li&gt;Takeaways&lt;/li&gt;
&lt;li&gt;Where to go next&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The problem with prose
&lt;/h2&gt;

&lt;p&gt;You ask the model to "extract the customer's name, email, and order total" and it replies: "Sure! The customer is Jane Doe (&lt;a href="mailto:jane@acme.io"&gt;jane@acme.io&lt;/a&gt;) and her order came to $429." Lovely for a human. Useless for your code. Now you are writing a regex to pull the email out, a second one for the dollar amount, and a third to handle the time it decided to answer in Spanish. Every prompt tweak breaks your parser.&lt;/p&gt;

&lt;p&gt;The fix is to stop treating the model as a chatbot and start treating it as a &lt;strong&gt;function that returns data&lt;/strong&gt;. Modern LLM APIs give you two tools for this: &lt;strong&gt;structured output&lt;/strong&gt; (the model must answer in a JSON shape you define) and &lt;strong&gt;tool calling&lt;/strong&gt; (the model can ask &lt;em&gt;your&lt;/em&gt; code to run a function and use the result). Get these right and the LLM becomes a reliable component you can wire into real software.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;Who this is for:&lt;/strong&gt; You have made a basic LLM API call (see &lt;a href="https://thesimplifiedtech.com/blog/working-with-the-llm-api" rel="noopener noreferrer"&gt;Working with the LLM API&lt;/a&gt;) and now you need machine-readable answers or you want the model to actually &lt;em&gt;do&lt;/em&gt; things, query a database, call an API, send an email. No ML background required. We use Python, but the concepts map to any SDK.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Two mental models
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Structured output is a form the model must fill in. Tool calling is a set of buttons the model is allowed to press.&lt;/p&gt;

&lt;p&gt;The whole article in one line&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Both are about &lt;strong&gt;constraining&lt;/strong&gt; the model. Left unconstrained, it generates the most plausible-sounding text. That is great for an essay and terrible for an integration. A form has labelled boxes and the model has to put something valid in each one. A button does nothing until pressed, and when pressed, &lt;em&gt;your&lt;/em&gt; code runs, not the model's imagination.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;In the real world&lt;/th&gt;
&lt;th&gt;In tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A paper form with labelled fields and checkboxes&lt;/td&gt;
&lt;td&gt;JSON schema / structured output, the model must return the exact shape&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Required fields you cannot leave blank&lt;/td&gt;
&lt;td&gt;Schema 'required' array, the model cannot omit them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A row of buttons on a dashboard&lt;/td&gt;
&lt;td&gt;Tool definitions, named functions the model may invoke&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pressing a button triggers a machine, not the operator&lt;/td&gt;
&lt;td&gt;Your backend executes the tool; the model never runs code itself&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Two ways to constrain a free-text model into something your code can trust.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The tool-calling loop
&lt;/h2&gt;

&lt;p&gt;The single most misunderstood thing about tool calling: &lt;strong&gt;the model does not run your code.&lt;/strong&gt; It only emits a request that says "please call &lt;code&gt;get_weather&lt;/code&gt; with &lt;code&gt;city=Amsterdam&lt;/code&gt;." Your program receives that request, runs the real function, and hands the result back. The model then continues with that fact in hand. It is a conversation that loops until the model has everything it needs to answer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fdXNlcigiVXNlciBwcm9tcHQ8YnIvPidXaGF0J3MgdGhlIHdlYXRoZXIgaW4gQW1zdGVyZGFtPyciKQogIGNsYXNzIG5fdXNlciBjbGllbnQ7CiAgbl9tb2RlbCgiTExNPGJyLz5EZWNpZGVzOiBuZWVkcyBhIHRvb2wiKQogIGNsYXNzIG5fbW9kZWwgY29tcHV0ZTsKICBuX2NhbGwoIlRvb2wgY2FsbCBlbWl0dGVkPGJyLz5nZXRfd2VhdGhlcihjaXR5KSIpCiAgY2xhc3Mgbl9jYWxsIHF1ZXVlOwogIG5fY29kZSgiWW91ciBjb2RlPGJyLz5FeGVjdXRlcyB0aGUgZnVuY3Rpb24iKQogIGNsYXNzIG5fY29kZSBjb21wdXRlOwogIG5fYXBpKCJXZWF0aGVyIEFQSTxici8-RXh0ZXJuYWwgc2VydmljZSIpCiAgY2xhc3Mgbl9hcGkgZXh0ZXJuYWw7CiAgbl9yZXN1bHQoIlJlc3VsdCByZXR1cm5lZDxici8-eyB0ZW1wQzogMTQgfSIpCiAgY2xhc3Mgbl9yZXN1bHQgZGF0YTsKICBuX2ZpbmFsKCJGaW5hbCBhbnN3ZXI8YnIvPk5hdHVyYWwtbGFuZ3VhZ2UgcmVwbHkiKQogIGNsYXNzIG5fZmluYWwgY2xpZW50OwogIG5fdXNlciAtLT58InByb21wdCArIHRvb2xzInwgbl9tb2RlbAogIG5fbW9kZWwgLS0-fCJlbWl0Inwgbl9jYWxsCiAgbl9jYWxsIC0tPnwieW91IGhhbmRsZSJ8IG5fY29kZQogIG5fY29kZSAtLi0-fCJmZXRjaCJ8IG5fYXBpCiAgbl9jb2RlIC0tPnwicnVuInwgbl9yZXN1bHQKICBuX3Jlc3VsdCAtLT58ImZlZWQgYmFjayJ8IG5fbW9kZWwKICBuX21vZGVsIC0tPnwiY29udGludWUifCBuX2ZpbmFs%3FbgColor%3D0d1017%26type%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FJSV7aW5pdDogeyJ0aGVtZSI6ImJhc2UiLCJ0aGVtZVZhcmlhYmxlcyI6eyJmb250RmFtaWx5IjoidWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCAtYXBwbGUtc3lzdGVtLCBTZWdvZSBVSSwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmIiwiZm9udFNpemUiOiIxNXB4IiwibGluZUNvbG9yIjoiIzZiNzY4NCIsInByaW1hcnlUZXh0Q29sb3IiOiIjZTZlZGYzIiwiZWRnZUxhYmVsQmFja2dyb3VuZCI6IiMwZDEwMTcifSwiZmxvd2NoYXJ0Ijp7ImN1cnZlIjoiYmFzaXMiLCJub2RlU3BhY2luZyI6NjAsInJhbmtTcGFjaW5nIjo3OCwicGFkZGluZyI6MTZ9fX0lJQpmbG93Y2hhcnQgTFIKICBjbGFzc0RlZiBjbGllbnQgZmlsbDojMmIyMTBhLHN0cm9rZTojZjU5ZTBiLHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkZTY4YTsKICBjbGFzc0RlZiBlZGdlIGZpbGw6IzA2MjUxYyxzdHJva2U6IzEwYjk4MSxzdHJva2Utd2lkdGg6MnB4LGNvbG9yOiM2ZWU3Yjc7CiAgY2xhc3NEZWYgY29tcHV0ZSBmaWxsOiMwNzI3MmEsc3Ryb2tlOiMyMmI4YjAsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojN2ZlOWRmOwogIGNsYXNzRGVmIGRhdGEgZmlsbDojMmExNTA4LHN0cm9rZTojZjk3MzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2ZkYmE3NDsKICBjbGFzc0RlZiBxdWV1ZSBmaWxsOiMyYTI0MDgsc3Ryb2tlOiNlYWIzMDgsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZmRlMDQ3OwogIGNsYXNzRGVmIG9ic2VydmFiaWxpdHkgZmlsbDojMWEyNzA4LHN0cm9rZTojODRjYzE2LHN0cm9rZS13aWR0aDoycHgsY29sb3I6I2JlZjI2NDsKICBjbGFzc0RlZiBleHRlcm5hbCBmaWxsOiMxYzE5MTcsc3Ryb2tlOiNhOGEyOWUsc3Ryb2tlLXdpZHRoOjJweCxjb2xvcjojZTdlNWU0OwogIG5fdXNlcigiVXNlciBwcm9tcHQ8YnIvPidXaGF0J3MgdGhlIHdlYXRoZXIgaW4gQW1zdGVyZGFtPyciKQogIGNsYXNzIG5fdXNlciBjbGllbnQ7CiAgbl9tb2RlbCgiTExNPGJyLz5EZWNpZGVzOiBuZWVkcyBhIHRvb2wiKQogIGNsYXNzIG5fbW9kZWwgY29tcHV0ZTsKICBuX2NhbGwoIlRvb2wgY2FsbCBlbWl0dGVkPGJyLz5nZXRfd2VhdGhlcihjaXR5KSIpCiAgY2xhc3Mgbl9jYWxsIHF1ZXVlOwogIG5fY29kZSgiWW91ciBjb2RlPGJyLz5FeGVjdXRlcyB0aGUgZnVuY3Rpb24iKQogIGNsYXNzIG5fY29kZSBjb21wdXRlOwogIG5fYXBpKCJXZWF0aGVyIEFQSTxici8-RXh0ZXJuYWwgc2VydmljZSIpCiAgY2xhc3Mgbl9hcGkgZXh0ZXJuYWw7CiAgbl9yZXN1bHQoIlJlc3VsdCByZXR1cm5lZDxici8-eyB0ZW1wQzogMTQgfSIpCiAgY2xhc3Mgbl9yZXN1bHQgZGF0YTsKICBuX2ZpbmFsKCJGaW5hbCBhbnN3ZXI8YnIvPk5hdHVyYWwtbGFuZ3VhZ2UgcmVwbHkiKQogIGNsYXNzIG5fZmluYWwgY2xpZW50OwogIG5fdXNlciAtLT58InByb21wdCArIHRvb2xzInwgbl9tb2RlbAogIG5fbW9kZWwgLS0-fCJlbWl0Inwgbl9jYWxsCiAgbl9jYWxsIC0tPnwieW91IGhhbmRsZSJ8IG5fY29kZQogIG5fY29kZSAtLi0-fCJmZXRjaCJ8IG5fYXBpCiAgbl9jb2RlIC0tPnwicnVuInwgbl9yZXN1bHQKICBuX3Jlc3VsdCAtLT58ImZlZWQgYmFjayJ8IG5fbW9kZWwKICBuX21vZGVsIC0tPnwiY29udGludWUifCBuX2ZpbmFs%3FbgColor%3D0d1017%26type%3Dpng" alt="One full turn of the tool-calling loop. The model and your code take turns until a final answer falls out." width="1418" height="314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One full turn of the tool-calling loop. The model and your code take turns until a final answer falls out.&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;You send the prompt plus tool definitions&lt;/strong&gt;: Each API call includes the user message AND a list of tools the model is allowed to use, name, description, and a JSON schema for the arguments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model decides&lt;/strong&gt;: It either answers directly, or it returns a 'tool_use' block: the tool name and the arguments it wants, as structured JSON. No prose answer yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your code executes the tool&lt;/strong&gt;: You match the tool name to a real Python function, validate the arguments, and run it. The model is paused, waiting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You return the result&lt;/strong&gt;: Append the tool result to the conversation (linked to the call's id) and send the whole thing back to the model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model continues, or loops again&lt;/strong&gt;: With the result in context it may answer, or request another tool. You repeat until it stops asking and produces a final answer.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Four levels of reliability
&lt;/h2&gt;

&lt;p&gt;There is a ladder from "hope it parses" to "guaranteed valid." Climb only as high as you need, strict schemas and tools cost more tokens and more setup. Match the technique to the job.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Technique&lt;/th&gt;
&lt;th&gt;What you get&lt;/th&gt;
&lt;th&gt;Reliability&lt;/th&gt;
&lt;th&gt;Use it when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Free text&lt;/td&gt;
&lt;td&gt;Natural-language prose&lt;/td&gt;
&lt;td&gt;Low, you parse strings yourself&lt;/td&gt;
&lt;td&gt;Human reads the output directly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSON mode&lt;/td&gt;
&lt;td&gt;Valid JSON, but any shape&lt;/td&gt;
&lt;td&gt;Medium, parses, fields not guaranteed&lt;/td&gt;
&lt;td&gt;Quick prototypes, loose shapes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strict JSON schema&lt;/td&gt;
&lt;td&gt;JSON matching your exact schema&lt;/td&gt;
&lt;td&gt;High, fields, types, enums enforced&lt;/td&gt;
&lt;td&gt;Extraction, classification, data into a DB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool calling&lt;/td&gt;
&lt;td&gt;Validated args for named functions&lt;/td&gt;
&lt;td&gt;High, plus the model can act, not just answer&lt;/td&gt;
&lt;td&gt;The model must DO something (query, send, fetch)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;From least to most constrained. Higher rows are cheaper and looser; lower rows are reliable and machine-ready.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;JSON mode is not schema enforcement:&lt;/strong&gt; Plain JSON mode guarantees the output &lt;em&gt;parses&lt;/em&gt;, not that it has the keys you expect. If you need &lt;code&gt;email&lt;/code&gt; to always be present and a string, use a strict &lt;strong&gt;schema&lt;/strong&gt;, not just JSON mode. Many a 2am bug is a missing field that "usually" appears.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Code: a tool definition and the loop
&lt;/h2&gt;

&lt;p&gt;Here is a complete, minimal tool-calling loop. The tool is described with a JSON schema for its arguments. The loop keeps running the model until it stops asking for tools, with a hard cap so a confused model can never spin forever.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tool_loop.py&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;anthropic&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;anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# 1. Describe the tool: name, when to use it, and an argument schema.
&lt;/span&gt;&lt;span class="n"&gt;TOOLS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;get_weather&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Get the current temperature for a city, in Celsius.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input_schema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;city&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;City name, e.g. &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Amsterdam&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;city&lt;/span&gt;&lt;span class="sh"&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;span class="c1"&gt;# 2. The real function the model is allowed to trigger.
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_weather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# In real life: call a weather API. Here we fake it.
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;city&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tempC&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;summary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cloudy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;TOOL_FNS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;get_weather&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;get_weather&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_turns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&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="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_turns&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;  &lt;span class="c1"&gt;# bounded loop, never unbounded!
&lt;/span&gt;        &lt;span class="n"&gt;resp&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;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-4-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TOOLS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&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;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stop_reason&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_use&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# No tool requested -&amp;gt; this is the final answer.
&lt;/span&gt;            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# 3. Run every tool the model asked for, collect the results.
&lt;/span&gt;        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assistant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&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;block&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&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;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_use&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TOOL_FNS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&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;fn&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown tool &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# validate in real code!
&lt;/span&gt;                &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&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="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_use_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;

        &lt;span class="c1"&gt;# 4. Feed the results back and loop.
&lt;/span&gt;        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Stopped: hit max_turns without a final answer.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s the weather in Amsterdam? Should I take an umbrella?&lt;/span&gt;&lt;span class="sh"&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 three things: the assistant's tool-call message is appended &lt;strong&gt;before&lt;/strong&gt; the result, each result is keyed to its &lt;code&gt;tool_use_id&lt;/code&gt;, and the loop has a &lt;code&gt;max_turns&lt;/code&gt; ceiling. Drop any one of those and the loop breaks subtly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code: structured output with a schema
&lt;/h2&gt;

&lt;p&gt;When you do not need the model to &lt;em&gt;act&lt;/em&gt;, you just want clean data, use a structured-output schema. Define the shape, ask the model to fill it, then &lt;strong&gt;validate before you trust it&lt;/strong&gt;. Pydantic makes the schema and the validation the same source of truth.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;extract.py&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EmailStr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ValidationError&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;anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# The form the model must fill in.
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;customer_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EmailStr&lt;/span&gt;
    &lt;span class="n"&gt;total_usd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;  &lt;span class="c1"&gt;# one of: low | normal | high
&lt;/span&gt;
&lt;span class="n"&gt;SCHEMA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;format&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total_usd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;priority&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;enum&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;low&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;normal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;high&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total_usd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;priority&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Trick: expose the schema AS a tool and force the model to call it.
# The 'arguments' it produces are your structured output.
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;resp&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;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-4-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;record_order&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Record the extracted order details.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input_schema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SCHEMA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="n"&gt;tool_choice&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;record_order&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;  &lt;span class="c1"&gt;# force it
&lt;/span&gt;        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Extract the order:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_use&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# NEVER skip this step. The schema guides the model; it does not
&lt;/span&gt;    &lt;span class="c1"&gt;# guarantee semantics. Validate before the data touches your DB.
&lt;/span&gt;    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;ValidationError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Model returned invalid order: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&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="nf"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Jane Doe (jane@acme.io) placed a rush order totalling $429.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_dump&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Common mistakes that cost hours
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No validation.&lt;/strong&gt; A schema &lt;em&gt;guides&lt;/em&gt; the model; it does not certify the data is sane. A &lt;code&gt;total_usd&lt;/code&gt; of -1 or an email of "n/a" can still come back. Always re-validate (Pydantic, zod, JSON-Schema) before the value reaches your database or an irreversible action.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trusting hallucinated arguments.&lt;/strong&gt; The model can invent a plausible-looking &lt;code&gt;user_id&lt;/code&gt; that does not exist, or a &lt;code&gt;city&lt;/code&gt; you never support. Treat tool arguments like untrusted user input: check ranges, enums, and existence before you act on them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unbounded tool loops.&lt;/strong&gt; Without a &lt;code&gt;max_turns&lt;/code&gt; cap, a confused model can call the same tool forever, burning tokens and money. Always bound the loop and log when you hit the ceiling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vague tool descriptions.&lt;/strong&gt; "Gets data" tells the model nothing about &lt;em&gt;when&lt;/em&gt; to use it. Write the description like a docstring for a junior dev: what it does, what each argument means, and when NOT to call it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting the result id.&lt;/strong&gt; Each &lt;code&gt;tool_result&lt;/code&gt; must reference the &lt;code&gt;tool_use_id&lt;/code&gt; of the call it answers. Mismatch them and the model loses the thread of which result belongs to which request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Side effects on retry.&lt;/strong&gt; If the model calls &lt;code&gt;send_email&lt;/code&gt; twice (it happens), you double-send. Make irreversible tools idempotent or require a human confirmation step.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The whole article in seven lines&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Treat the LLM as a function that returns data, not a chatbot that returns prose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured output&lt;/strong&gt; = a form the model must fill in. &lt;strong&gt;Tool calling&lt;/strong&gt; = buttons it may press.&lt;/li&gt;
&lt;li&gt;JSON mode guarantees it &lt;em&gt;parses&lt;/em&gt;; a strict &lt;strong&gt;schema&lt;/strong&gt; guarantees the fields and types.&lt;/li&gt;
&lt;li&gt;The model never runs your code, it emits a request, your code executes, you feed the result back.&lt;/li&gt;
&lt;li&gt;The loop is: prompt+tools → tool call → execute → return result → continue → final answer.&lt;/li&gt;
&lt;li&gt;Always &lt;strong&gt;validate&lt;/strong&gt; model output before it touches a database or an irreversible action.&lt;/li&gt;
&lt;li&gt;Always &lt;strong&gt;bound&lt;/strong&gt; the tool loop with a max-turns ceiling.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;Structured output and tool calling are the two primitives that turn an LLM from a text generator into a software component. The natural next step is to chain many tool calls together with memory and a goal, that is what an agent is.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Came in cold?&lt;/strong&gt; Start with &lt;a href="https://thesimplifiedtech.com/blog/working-with-the-llm-api" rel="noopener noreferrer"&gt;Working with the LLM API&lt;/a&gt; for the request/response basics this article builds on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ready to go further?&lt;/strong&gt; &lt;a href="https://thesimplifiedtech.com/blog/building-ai-agents" rel="noopener noreferrer"&gt;Building AI Agents&lt;/a&gt; wires this loop into a goal-driven agent with planning and memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Want the full track?&lt;/strong&gt; Follow the &lt;a href="https://thesimplifiedtech.com/career-paths/ai-engineer" rel="noopener noreferrer"&gt;AI Engineer path&lt;/a&gt; from foundations to production.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://thesimplifiedtech.com/blog/structured-output-and-tool-calling" rel="noopener noreferrer"&gt;TheSimplifiedTech&lt;/a&gt;, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>llm</category>
      <category>toolcalling</category>
    </item>
  </channel>
</rss>
