{
  "version": "https://jsonfeed.org/version/1.1",
  "title": "Anass Ez-zouaine | Backend, AI & Shopify Engineer",
  "home_page_url": "https://ansezz.com/",
  "feed_url": "https://ansezz.com/feed.json",
  "description": "Senior backend, software architect, and AI engineer. 14+ years shipping Laravel SaaS, Shopify Plus apps, and AI features (Claude, MCP, RAG). Remote-first.",
  "language": "en",
  "authors": [
    {
      "name": "Anass Ez-zouaine",
      "url": "https://ansezz.com"
    }
  ],
  "items": [
    {
      "id": "https://ansezz.com/blog/laravel-sticky-connections-queue-workers/",
      "url": "https://ansezz.com/blog/laravel-sticky-connections-queue-workers/",
      "title": "Your Laravel queue workers may be skipping your read replicas",
      "summary": "With sticky read/write connections, one job that writes can push every later job in the same worker onto the primary database. Laravel 13.36 adds an opt-in reset. Here is how it works and when to turn it on.",
      "content_html": "<p>You added read replicas so the primary database could rest. Then you look at the metrics and the primary is still doing most of the reads. If you run queue workers with <code>sticky</code> turned on, the workers may be the reason.</p>\n\n- With `sticky` on, a write sends later reads on that connection to the write (primary) database.\n- A queue worker is one long process, so one job that writes can pin the reads of every later job in that worker to the primary.\n- Laravel 13.36 adds `Worker::$flushStickyConnections`, an opt-in reset before each job.\n- Turn it on unless a job depends on reading what an earlier, unrelated job wrote in the same worker.\n\n<h2>What <code>sticky</code> does</h2>\n<p>Laravel lets you split a database connection into a <code>read</code> side and a <code>write</code> side. Selects go to the read hosts, and inserts, updates and deletes go to the write host (<a href=\"https://laravel.com/docs/13.x/database#read-and-write-connections\">Laravel docs</a>).</p>\n<p>Replicas lag a little behind the primary. So if you write a row and read it back right away from a replica, it may not be there yet. The <code>sticky</code> option fixes that. The docs describe it this way: if a write has run \"during the current request cycle\", any further reads use the write connection, so you can read back what you just wrote (<a href=\"https://laravel.com/docs/13.x/database#the-sticky-option\">Laravel docs</a>).</p>\n<pre><code>'mysql' =&gt; [\n    'driver' =&gt; 'mysql',\n    'read' =&gt; ['host' =&gt; ['10.0.0.11', '10.0.0.12']],\n    'write' =&gt; ['host' =&gt; ['10.0.0.10']],\n    'sticky' =&gt; true,\n    // ...\n],\n</code></pre>\n<p>In a web request this is a good trade. The request ends, the process state resets, and the next request starts on the replicas again.</p>\n<h2>Why queue workers are different</h2>\n<p>A queue worker does not end after one job. <code>php artisan queue:work</code> boots once and keeps running jobs until it is restarted. There is no \"request cycle\" boundary between jobs.</p>\n<p>The Laravel News write-up of the release puts it plainly: \"a single job that writes can send the reads of every later job in that worker to the write connection\" (<a href=\"https://laravel-news.com/laravel-13-36-0\">Laravel News</a>).</p>\n<p>The pull request that added the fix gives a concrete picture. For a worker with a high memory limit, one job that writes \"could mean for the next 10,000 jobs it uses the write connection\", or until the next deploy restarts the worker, \"which means we actually lose the benefit of having replicas\" (<a href=\"https://github.com/laravel/framework/pull/61915\">PR #61915</a>).</p>\n<figure>\n<p>Most job workloads write something. So in practice, almost every long-running worker ends up reading from the primary.</p>\n<h2>The fix in Laravel 13.36</h2>\n<p>Laravel v13.36.0 was released on October 13, 2026 (<a href=\"https://github.com/laravel/framework/releases/tag/v13.36.0\">GitHub release</a>). It includes a new static flag on the queue worker, merged on October 7 (<a href=\"https://github.com/laravel/framework/pull/61915\">PR #61915</a>):</p>\n<pre><code>use Illuminate\\Queue\\Worker;\n\n// AppServiceProvider::boot()\nWorker::$flushStickyConnections = true;\n</code></pre>\n<p>With the flag on, the worker resets the sticky state before each job. Each job starts on the replicas, and only switches to the primary for reads after it writes something itself.</p>\n<p>It is off by default. The reason given is that \"a job could depend on reading what an earlier job in the same worker wrote\" (<a href=\"https://laravel-news.com/laravel-13-36-0\">Laravel News</a>). The PR author suggests it could become the default in Laravel 14, but did not want to break existing apps in a minor release (<a href=\"https://github.com/laravel/framework/pull/61915\">PR #61915</a>).</p>\n\n  <div>\n<p>Job 1 writes. Jobs 2 to N in that worker read from the primary until the worker restarts.</p>\n  </div>\n  <div>\n<p>Job 1 writes. Job 2 starts fresh on the replicas. It only reads from the primary after its own first write.</p>\n  </div>\n\n<h2>Should you turn it on?</h2>\n<p>Ask one question: does any job read data that a different job wrote moments before, and only works because both ran in the same worker?</p>\n<p>If the answer is yes, that is a hidden dependency. The PR author makes the same point: if jobs depend on other jobs, \"they should be a chain or a batch, not quietly relying on whichever worker process happened to pop them\" (<a href=\"https://github.com/laravel/framework/pull/61915\">PR #61915</a>). With several workers, the second job can already land on a different process, so the dependency is fragile today.</p>\n<p>If the answer is no, turning the flag on is a small change with a clear upside: your workers start using the replicas you already pay for.</p>\n<p>A simple rollout:</p>\n<ol>\n<li>Upgrade to Laravel 13.36 or later.</li>\n<li>Add the flag in <code>AppServiceProvider::boot()</code>.</li>\n<li>Deploy, then restart your workers so they pick up the change (<code>php artisan queue:restart</code>).</li>\n<li>Watch read traffic on the primary and on the replicas. Reads should move toward the replicas.</li>\n<li>Watch failed jobs for \"model not found\" style errors. Those point to a job reading a replica before replication caught up.</li>\n</ol>\n<figure>\n<h2>Watch for replica lag</h2>\n<p>The flag does not remove replica lag. It only stops one job's write from deciding where the next job reads.</p>\n<p>If a job is dispatched right after a web request writes a row, and the job reads that row from a replica, it can still miss it. That was true before this change for the first job in a fresh worker, too. For those jobs, read from the write connection on purpose, for example with <code>Model::onWriteConnection()</code> (<a href=\"https://api.laravel.com/docs/13.x/Illuminate/Database/Eloquent/Model.html\">API docs</a>), or dispatch the job with <code>afterCommit()</code> so it only runs once the transaction is committed (<a href=\"https://laravel.com/docs/13.x/queues#jobs-and-database-transactions\">Laravel docs</a>).</p>\n<h2>Other queue fixes in the same release</h2>\n<p>The same release has a few more changes for people running workers (<a href=\"https://laravel-news.com/laravel-13-36-0\">Laravel News</a>, <a href=\"https://github.com/laravel/framework/releases/tag/v13.36.0\">GitHub release</a>):</p>\n<table>\n<thead>\n<tr>\n<th>Change</th>\n<th>What it means</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>#[DeleteWhenMissingModels]</code> and <code>#[FailOnTimeout]</code> now work on queued mailables</td>\n<td>A mail for a deleted model is dropped instead of failing, and a timed out mail is not retried and sent twice.</td>\n</tr>\n<tr>\n<td>Redis queue notification pushes are batched</td>\n<td>Fewer round trips to Redis when pushing many notifications.</td>\n</tr>\n<tr>\n<td>PgBouncer SSL EOF errors count as lost connections</td>\n<td>Laravel can reconnect instead of failing the job.</td>\n</tr>\n<tr>\n<td>Mercure broadcasts run synchronously by default</td>\n<td>Set <code>MERCURE_ASYNC=true</code> to queue them again.</td>\n</tr>\n</tbody>\n</table>\n\n- `sticky` is built for request cycles, and a queue worker has no request cycle between jobs.\n- One writing job can pin a worker's reads to the primary until the worker restarts.\n- Laravel 13.36 adds `Worker::$flushStickyConnections = true` to reset that before each job.\n- If a job depends on another job's write, make it a chain or a batch, then turn the flag on.\n- Measure read traffic on the primary before and after.\n\n<p>Have you checked where your workers actually read from?</p>\n<h2>Sources</h2>\n<ul>\n<li><a href=\"https://laravel-news.com/laravel-13-36-0\">Laravel 13.36 release notes, Laravel News</a></li>\n<li><a href=\"https://github.com/laravel/framework/releases/tag/v13.36.0\">laravel/framework v13.36.0 release</a></li>\n<li><a href=\"https://github.com/laravel/framework/pull/61915\">PR #61915: Allow queue workers to flush sticky connections between jobs</a></li>\n<li><a href=\"https://laravel.com/docs/13.x/database#read-and-write-connections\">Laravel 13.x docs: read and write connections, the sticky option</a></li>\n<li><a href=\"https://laravel.com/docs/13.x/queues#jobs-and-database-transactions\">Laravel 13.x docs: jobs and database transactions</a></li>\n<li><a href=\"https://api.laravel.com/docs/13.x/Illuminate/Database/Eloquent/Model.html\">Laravel API: Eloquent Model::onWriteConnection</a></li>\n</ul>\n</figure></figure>",
      "date_published": "2026-10-15T00:00:00.000Z",
      "date_modified": "2026-10-15T00:00:00.000Z",
      "tags": [
        "laravel",
        "laravel",
        "databases",
        "performance",
        "scaling"
      ],
      "image": "https://ansezz.com/blog/laravel-sticky-connections-queue-workers/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/ai-code-quality-gates/",
      "url": "https://ansezz.com/blog/ai-code-quality-gates/",
      "title": "AI writes the code. Your job is the gates.",
      "summary": "AI made code cheap. The engineer's job is the gate stack bad code cannot pass: tests, mutation testing, PHPStan, required CI checks, monitoring and rollback.",
      "content_html": "<p>An AI can write 10,000 lines of code in a minute. I don't care. I care whether those 10,000 lines survive my constraints.</p>\n\n- AI made writing code cheap, so the scarce work is deciding what is allowed to merge and ship.\n- Stack your gates from cheap to expensive: static analysis, unit and integration tests, mutation testing, required CI checks, then monitoring with a fast rollback.\n- Keep human review for design and risk, and turn every comment you repeat into a rule the pipeline enforces.\n\n<p>Code generation stopped being the slow part of my week. An agent can scaffold a Laravel module, its migrations and its tests before my coffee cools.</p>\n<p>That speed is real, and I use it every day. It also changed the question. \"Can we write this?\" is cheap now. \"Should this reach production?\" is the expensive one.</p>\n<p>So I stopped judging my work by how much code I write. I judge the system around the code: what it refuses, how fast it refuses it, and how fast it recovers when something slips through.</p>\n<h2>Cheap code moves the job</h2>\n<p>When code was slow to write, the writing itself filtered out bad ideas. You thought twice before typing 400 lines by hand.</p>\n<p>That filter is gone. Volume goes up, and every weak spot in your process gets hit more often.</p>\n<p>I covered the trade-off in <a href=\"https://ansezz.com/blog/ai-vs-traditional-development/\">AI vs traditional development</a> and the loop itself in <a href=\"https://ansezz.com/blog/agentic-workflows-vibe-coding/\">agentic workflows</a>. This post is the next step: the concrete gates I put between generated code and real users.</p>\n<p>Think of them as a row of doors. Each one is cheap to run, catches one kind of failure, and says no without caring who wrote the code. An agent, a teammate, or me at 1am: same doors.</p>\n\n  <div>Writes most lines by hand. Quality depends on how careful you were that day. Review catches what it can.</div>\n  <div>Designs the checks every change must pass. Quality depends on the gates, and they run the same way on every push.</div>\n\n<h2>Gate 1: static analysis and types</h2>\n<p>The cheapest gate runs before any test. PHPStan reads your code and finds whole classes of bugs without executing it. Larastan teaches it about Laravel: Eloquent models, facades and container bindings.</p>\n<p>PHPStan has levels from 0 to 10. Level 10 arrived with PHPStan 2.0 and treats every <code>mixed</code> type strictly, including the implicit ones you get from missing types.</p>\n<p>On an existing app you won't jump to level 10 on day one, and you don't need to.</p>\n<p>Generate a baseline first, then add it to <code>includes</code>. Old errors are recorded and new code has to meet the higher bar.</p>\n<pre><code>vendor/bin/phpstan analyse --generate-baseline\n</code></pre>\n<pre><code># phpstan.neon\nincludes:\n    - vendor/larastan/larastan/extension.neon\n    - phpstan-baseline.neon\n\nparameters:\n    paths:\n        - app/\n    level: 8\n</code></pre>\n<p>Then ratchet. Every few weeks, clear part of the baseline, and raise the level by one when it gets small. That file is a to-do list that can only get shorter.</p>\n<p>Agents are happy here. Point one at a failing PHPStan run and it will fix types all afternoon without complaint. That's exactly the work I want to hand over.</p>\n\nRun PHPStan one level above your current setting with `--generate-baseline`, commit the baseline, and raise the level in CI. From today, new code meets the stricter rule.\n\n<h2>Gate 2: tests that describe behavior</h2>\n<p>Unit tests pin down the rules: pricing, proration, state changes, the logic that costs money when it's wrong. Integration tests (Laravel calls them feature tests) prove the pieces work together: the route, the policy, the database and the queued job.</p>\n<p>I lean on feature tests for AI-written code. An agent can make every unit test pass and still wire the controller to the wrong policy. A feature test that hits the real route as a real user catches that.</p>\n<p>Pest also gives you architecture tests, which turn \"we don't do that here\" into a failing build:</p>\n<pre><code>// tests/ArchTest.php\narch()-&gt;preset()-&gt;laravel();\narch()-&gt;preset()-&gt;security();\n\narch('no debug calls ship')\n    -&gt;expect('App')\n    -&gt;not-&gt;toUse(['dd', 'dump']);\n</code></pre>\n<p>On an older app, start with <code>-&gt;ignoring()</code> for the folders you haven't cleaned up yet.</p>\n<p>When I write the spec first, I write it as tests. The agent gets a red suite and a clear finish line. If it can't turn the suite green honestly, I usually learn something about my spec.</p>\n<h2>Gate 3: mutation testing</h2>\n<p>Coverage tells you a line ran. It doesn't tell you a test would notice if that line were wrong. That gap is where generated tests like to hide, with assertions like \"the result is not null\".</p>\n<p>Mutation testing closes it. The tool changes your code on purpose (flips a <code>&gt;</code> to <code>&gt;=</code>, drops a method call, returns an empty array) and reruns your tests. If the tests still pass, that mutant escaped, and you just found a test that checks nothing.</p>\n<figure>\n<p>Pick the tool for your stack:</p>\n<table>\n<thead>\n<tr>\n<th>Stack</th>\n<th>Tool</th>\n<th>Fail the build with</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>PHP on Pest</td>\n<td>Pest's built-in <code>--mutate</code></td>\n<td><code>--min=70</code></td>\n</tr>\n<tr>\n<td>PHP on PHPUnit</td>\n<td>Infection</td>\n<td><code>--min-msi=70</code></td>\n</tr>\n<tr>\n<td>JavaScript or TypeScript</td>\n<td>StrykerJS</td>\n<td><code>thresholds.break</code></td>\n</tr>\n</tbody>\n</table>\n<p>One detail to know before you wire this up: Infection's Pest support covered Pest 1 only. From Pest 2 on, Pest runs its own mutation engine, so a Pest suite uses <code>pest --mutate</code>, and it mutates the classes your tests name with <code>covers()</code> or <code>mutates()</code>.</p>\n<p>On a PHPUnit suite, Infection is the usual choice. Its <code>--git-diff-lines</code> flag mutates only the lines a pull request touched, which keeps CI fast on a big codebase.</p>\n<p>For JavaScript, Stryker exits with code 1 when the score drops below <code>break</code>:</p>\n<pre><code>{\n  \"thresholds\": { \"high\": 80, \"low\": 60, \"break\": 60 }\n}\n</code></pre>\n<p>The 70 and 60 in these examples are starting points. Pick the score your suite hits today, gate on it, and raise it as the tests improve. Why this matters so much for agent-written tests is in <a href=\"https://ansezz.com/blog/testing-ai-generated-code/\">trust is not a QA strategy</a>.</p>\n<h2>Gate 4: CI that nobody can skip</h2>\n<p>All of this is a suggestion until the merge button respects it. Here is the workflow I start with on a Laravel app that uses Pest:</p>\n<pre><code>name: quality-gates\non: [pull_request]\n\njobs:\n  static:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v7\n      - uses: shivammathur/setup-php@v2\n        with:\n          php-version: \"8.4\"\n      - run: composer install --no-interaction --prefer-dist\n      - run: vendor/bin/phpstan analyse --no-progress\n\n  tests:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v7\n      - uses: shivammathur/setup-php@v2\n        with:\n          php-version: \"8.4\"\n      - run: composer install --no-interaction --prefer-dist\n      - run: cp .env.example .env &amp;&amp; php artisan key:generate\n      - run: vendor/bin/pest --parallel\n\n  mutation:\n    needs: tests\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v7\n        with:\n          fetch-depth: 0\n      - uses: shivammathur/setup-php@v2\n        with:\n          php-version: \"8.4\"\n          coverage: pcov\n      - run: composer install --no-interaction --prefer-dist\n      - run: cp .env.example .env &amp;&amp; php artisan key:generate\n      - run: vendor/bin/pest --mutate --parallel --min=70\n      # PHPUnit suite? The same gate with Infection:\n      # vendor/bin/infection --git-diff-lines --git-diff-base=origin/main --min-msi=70 --threads=max\n</code></pre>\n<p>The <code>mutation</code> job only has work to do once your tests name the classes they cover with <code>covers()</code> (or <code>mutates()</code>), so add those as you write tests.</p>\n<p>Then open branch protection (or a ruleset) for <code>main</code> and turn on \"Require status checks to pass before merging\". Add all three jobs. Also tick \"Require branches to be up to date before merging\", so each pull request is tested against the code it will actually land on.</p>\n<p>Now nobody merges red. No agent, no teammate in a hurry, and no version of me on a deadline. The wider picture of the pipeline is in <a href=\"https://ansezz.com/blog/ci-vs-cd/\">CI vs CD</a>.</p>\n\nOpen your repo settings and check which jobs are required on `main`. A quality job that isn't on that list is decoration. Add it today.\n\n<h2>Gate 5: production is a gate too</h2>\n<p>Some bugs only show up with real data and real traffic. Your tests won't catch the query that is fine with 200 rows and slow with two million. So the last gate sits after the deploy.</p>\n<p>I want three things there:</p>\n<ul>\n<li><strong>Error tracking.</strong> Sentry's Laravel SDK reports unhandled exceptions with their stack trace as they happen. I hear about the bug before the support ticket arrives.</li>\n<li><strong>A smoke check.</strong> Laravel 11 and later ship a <code>/up</code> health route that returns 200 when the app boots. Hit it right after deploy and fail the pipeline if it doesn't answer.</li>\n<li><strong>A rollback you have practiced.</strong> Keep the previous build ready to switch back to. If rolling back needs a meeting, it's too slow.</li>\n</ul>\n<pre><code>curl --fail --silent --max-time 10 https://app.example.com/up || ./rollback.sh\n</code></pre>\n<p>Monitoring is also how the gates improve. When something escapes to production, I fix it, then I ask which earlier gate should have caught it, and I add the test or the rule there. Every incident should leave the doors tighter. The difference between having logs and actually being told is in <a href=\"https://ansezz.com/blog/logging-vs-monitoring/\">logging vs monitoring</a>.</p>\n<figure>\n<h2>Code review, when it matters</h2>\n<p>With the machines doing the mechanical checks, review changes shape. I don't read every line hunting for missing types anymore. The pipeline already did that, on every file, with the same attention.</p>\n<p>My review time goes to the questions no tool answers. Should this feature exist? Does this new queue or table fit the system? What happens to the money if this webhook arrives twice?</p>\n<p>GitHub can route that attention for you. A <code>CODEOWNERS</code> file plus \"Require review from Code Owners\" means risky paths wait for a human, and everything else merges on green:</p>\n<pre><code># .github/CODEOWNERS\n/database/migrations/  @your-org/leads\n/app/Policies/         @your-org/leads\n/app/Billing/          @your-org/leads\n</code></pre>\n<p>My full read list, and how I handle agent-sized diffs, is in <a href=\"https://ansezz.com/blog/stop-reading-code-ai-review/\">stop reading every line of AI-generated code</a>.</p>\n<p>Generating code is the easy part now. The craft is in the system that decides what ships.</p>\n\nWrite a `CODEOWNERS` file with the three paths where a mistake costs real money or data, and turn on code owner review for them. Let the rest merge on green checks.\n\n<h2>What moving up the stack looks like on a Tuesday</h2>\n<p>\"Moving engineers up the stack\" sounds like a conference slide. Here is what it means in my actual week:</p>\n<ul>\n<li><strong>Less boilerplate.</strong> Agents write the CRUD, the DTOs, the form requests and the first draft of the tests. I don't miss typing them.</li>\n<li><strong>More specs.</strong> I write acceptance tests and invariants before the agent starts. A refund never exceeds the captured amount. A discount never drops a total below zero.</li>\n<li><strong>More pipeline work.</strong> I treat the CI config like product code. It gets reviewed, versioned and improved whenever an incident shows a gap.</li>\n<li><strong>More design.</strong> Schema choices, module boundaries, what goes on a queue. These decisions outlive any single file, and no gate can make them for you.</li>\n<li><strong>Fewer heroics.</strong> When the gates are good, a Friday deploy is boring. Boring is the goal.</li>\n</ul>\n<p>The engineers who do well with AI can say exactly what \"correct\" means, then make a machine check it on every push.</p>\n\nPick one feature from this sprint and write its acceptance tests before any code exists, yours or an agent's. Then compare that review with your last one.\n\n\n- Cheap code moves the hard work to deciding what may merge and ship.\n- Order your gates from cheap to expensive: static analysis, tests, mutation testing, required CI checks, then monitoring and rollback.\n- Use a baseline and a ratchet so standards rise without a big rewrite.\n- Spend human review on design and risk, and route it with CODEOWNERS.\n- Every bug that escapes should add a rule to an earlier gate.\n\n<p>If you want an outside look at which of these gates your Laravel pipeline is missing, <a href=\"https://ansezz.com/services/architecture-audit/\">my architecture audit covers the code, the CI and the deploy path</a>.</p>\n<p>Which gate is missing from your pipeline right now?</p>\n</figure></figure>",
      "date_published": "2026-10-04T00:00:00.000Z",
      "date_modified": "2026-10-04T00:00:00.000Z",
      "tags": [
        "ai",
        "code-quality",
        "ci-cd",
        "laravel",
        "ai-engineering"
      ],
      "image": "https://ansezz.com/blog/ai-code-quality-gates/hero-7845791c.webp"
    },
    {
      "id": "https://ansezz.com/blog/chatbots-answered-bots-now-do-the-work/",
      "url": "https://ansezz.com/blog/chatbots-answered-bots-now-do-the-work/",
      "title": "Chatbots answered questions. Bots now do the work.",
      "summary": "The first AI wave gave us chatbots that answer. The new wave gives us bots that do the work while we're away. What changes, what still needs a person, and who should get ready.",
      "content_html": "<p>Chatbots made answers cheap. Bots are making the work around the answers cheap.</p>\n\n- The chatbot wave made getting an answer cheap, but you still did the work around it.\n- The new bots wake up on their own, use your tools, and stop at a draft for you.\n- Taste, sends in your name, costly decisions and setting the rules still need a person.\n\n<p>A few years ago, AI meant a chat box. You asked a question, you got an answer, and then you did the work yourself.</p>\n<p>It was useful, but you were still the one who opened the apps, wrote the email and pressed send.</p>\n<p>That is changing fast. In the last few months, several big AI companies shipped some kind of bot that works on its own.</p>\n<p>It runs on a schedule, watches your inbox or your repo, uses your apps, and comes back with the work done or a draft ready for you. You don't have to be in the chat for it to happen.</p>\n<p>I think this is the next big wave, and it will touch a lot more people than the first one did.</p>\n<h2>Wave one: answers</h2>\n<p>The chatbot wave made one thing cheap: getting an answer. Explaining code, summarizing a long document, writing a first draft.</p>\n<p>All of that went from minutes or hours to seconds.</p>\n<p>But the job around the answer stayed the same. You still had to:</p>\n<ul>\n<li>notice that something needed doing</li>\n<li>open the right tool</li>\n<li>copy the answer into it</li>\n<li>follow up later</li>\n</ul>\n<p>The chatbot waited for you. If you didn't ask, nothing happened.</p>\n<h2>Wave two: work</h2>\n<p>The new bots flip that. You don't ask them each time. You give them an area and a set of rules once, and they keep going.</p>\n\n  <div>\n<p>The chatbot waited for you. If you didn't ask, nothing happened.</p>\n  </div>\n  <div>\n<p>You give the bot an area and a set of rules once, and it keeps going.</p>\n  </div>\n\n<p>In practice that looks like:</p>\n<ul>\n<li><strong>They wake up on their own.</strong> On a schedule, or when something happens: a new email, a failed build, a new comment.</li>\n<li><strong>They use your tools.</strong> Inbox, calendar, docs, code, a browser. Not just text in a chat window.</li>\n<li><strong>They finish the job or stop at the right point.</strong> A draft waiting for you, a pull request ready to review, a short note saying nothing changed.</li>\n</ul>\n<figure>\n<p>You can read how two of these products compare in <a href=\"https://ansezz.com/blog/grok-bot-vs-openai-dots\">Grok Bot vs OpenAI dots</a>, and how I set mine up in <a href=\"https://ansezz.com/blog/run-your-business-with-ai-assistants\">Run your business with one AI assistant per area</a>.</p>\n\nPick one area and write its rules down once, so a bot could keep going without you asking each time.\n\n<h2>What changed in my week</h2>\n<p>I run most of my week this way now. One bot looks after my website and blog. One handles social posts. One handles my inbox.</p>\n<p>They talk to each other in a shared chat, and I step in when they need a decision.</p>\n<p>The biggest change is not speed.</p>\n<p>Before, I was the one carrying context from one app to the next. Now the bots carry it, and I mostly review and decide.</p>\n<p>A normal day looks like this: I open the chat, read what got done overnight, approve a few drafts, say no to one or two, and give a new idea.</p>\n<p>That's most of the admin part of my day.</p>\n<h2>What still needs you</h2>\n<p>Bots doing the work does not mean bots running your life. A few things still need a person, and I think they will for a while:</p>\n<ul>\n<li><strong>Taste.</strong> A bot can write ten versions of a post. Picking the one that sounds like you is still your job.</li>\n<li><strong>Sending things in your name.</strong> Anything that goes to a real person should pass through you first. Good bots draft. You press send.</li>\n<li><strong>Decisions with real cost.</strong> Money, contracts, deleting things, saying no to a client.</li>\n<li><strong>Setting the rules.</strong> A bot is only as good as the scope you give it. Vague rules give you vague work.</li>\n</ul>\n<figure>\n<p>If anything, the skill that matters most now is being clear: clear about what each bot owns, what it may never touch, and when it should stop and ask.</p>\n\nFor one bot, write three lines: what it owns, what it may never touch, and when it should stop and ask.\n\n<h2>Who should get ready</h2>\n<p>Anyone whose day is mostly moving information between tools. That covers a lot of jobs:</p>\n<ul>\n<li>freelancers and small business owners who do their own admin</li>\n<li>developers who spend half the day on tickets, reviews and updates</li>\n<li>marketers and content people who plan, write and schedule</li>\n<li>assistants, coordinators and operations people</li>\n</ul>\n<p>This doesn't mean those jobs go away. It means the job moves. Less doing the steps, more deciding what the steps should be and checking the result.</p>\n<p>People who learn to set up and run a small team of bots will get a lot more done than people who don't.</p>\n\nLook at your last week and mark the parts that were mostly moving information between tools. Those are the steps to hand over first.\n\n<h2>How to start small</h2>\n<p>You don't need a big setup to try this.</p>\n<ol>\n<li>Pick one boring task you do every week.</li>\n<li>Give one bot that task and nothing else.</li>\n<li>Ask it to draft, not send.</li>\n<li>Review its work for two weeks.</li>\n<li>If it holds up, give it the next task.</li>\n</ol>\n<p>That's it. Start with one area, keep the rules strict, and add more once you trust it.</p>\n\nPick one boring task you do every week and give one bot that task and nothing else.\n\n\nChatbots made answers cheap. Bots are making the work around the answers cheap.\n<p>The people who do well in this wave won't be the ones who type the best prompts. They'll be the ones who are best at deciding what should happen, setting clear rules, and saying yes or no quickly.\n</p><p></p>\n<p>Which boring weekly task would you hand to a bot first?</p>\n</figure></figure>",
      "date_published": "2026-10-03T00:00:00.000Z",
      "date_modified": "2026-10-03T00:00:00.000Z",
      "tags": [
        "ai",
        "ai",
        "agentic-ai",
        "career"
      ],
      "image": "https://ansezz.com/blog/chatbots-answered-bots-now-do-the-work/hero-a99867bb.webp"
    },
    {
      "id": "https://ansezz.com/blog/grok-bot-vs-openai-dots/",
      "url": "https://ansezz.com/blog/grok-bot-vs-openai-dots/",
      "title": "Grok Bot vs OpenAI dots: a team of bots or one agent",
      "summary": "OpenAI dots and Grok Bot both give an AI agent its own cloud computer. Here is how they differ on teams, control, routines and access, and which fits your work.",
      "content_html": "<p>One agent that does everything, or a team where each bot has one job? That is the real choice between OpenAI dots and Grok Bot.</p>\n\n- Both products now give an agent its own cloud computer, background work and approvals.\n- Dots start as one personal agent. Grok Bot starts as a team of focused bots.\n- The best test is one real weekly task, run for a week.\n\n<p>On 29 September, OpenAI launched dots at DevDay.</p>\n<p>A day or two later, one founder posted that he had spent months building his business into a monorepo full of partitioned context, skills, automations and MCP connections. After 36 hours with dots, he thought most of it might not be needed anymore.</p>\n<p>That post hit home, because I run my own work the other way: a set of Grok Bot assistants, each with one job.</p>\n<p>So the honest question is not \"which one is smarter.\" It is \"which shape of agent fits the way you work.\" This post compares the two based on what each product has published, and where my own daily use backs it up.</p>\n<h2>At a glance</h2>\n<table>\n<thead>\n<tr>\n<th></th>\n<th>Dots</th>\n<th>Grok Bot</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Starting shape</td>\n<td>One primary dot, named and personalized</td>\n<td>A team of Bots, one per area of work</td>\n</tr>\n<tr>\n<td>Control</td>\n<td>Custom Rules plus an Auto-review layer</td>\n<td>Scopes, drafts and approvals</td>\n</tr>\n<tr>\n<td>Recurring work</td>\n<td>Goals that keep going in the background</td>\n<td>Named skills and routines</td>\n</tr>\n<tr>\n<td>Where you talk</td>\n<td>ChatGPT, voice calls, Slack, Microsoft Teams</td>\n<td>Desktop app, iPhone app, group chats</td>\n</tr>\n</tbody>\n</table>\n<h2>What both products have in common</h2>\n<p>The headline idea is now the same on both sides.</p>\n\n  <div>The agent is a chat box.</div>\n  <div>The agent is a worker with its own computer.</div>\n\n<ul>\n<li><strong>A cloud computer per agent.</strong> Dots run on their own cloud computer with a browser, and you can open it to inspect the work. Grok Bot Bots work on a persistent cloud computer with a browser, filesystem and terminal.</li>\n<li><strong>Always on.</strong> Both keep working between conversations and while your laptop is closed.</li>\n<li><strong>Connected apps.</strong> Dots reach more than 4,000 apps through OpenAI's plugin ecosystem. Grok Bot uses connectors (MCP servers) where they exist and drives the browser for everything else.</li>\n<li><strong>Approval for sensitive actions.</strong> Both come back to you before risky steps instead of doing everything silently.</li>\n<li><strong>Learning over time.</strong> Both keep context about you and get better with feedback.</li>\n</ul>\n\nWrite down what you want an agent to do. If it is one capable agent that books, researches and drafts, either one will feel like a big step up from a chatbot.\n\n<h2>The real difference: one agent or a team</h2>\n<p>This is the part that matters most.</p>\n<p><strong>Dots start with one.</strong> At launch you get one primary dot, named and personalized.</p>\n<p>OpenAI says you will be able to add more dots later, and it is piloting \"specialist dots\" for organizations with their own identity and access. But today, for most people, it is one agent doing everything.</p>\n<p><strong>Grok Bot starts with a team.</strong> You create as many Bots as you have areas of work. Each one gets a name, one job and its own memory.</p>\n<p>Bots can message each other, share context in group chats and hand work over, so you are not the router between them.</p>\n<figure>\n<h3>Why a team helps</h3>\n<p>One agent that does everything slowly becomes a catch-all. Its memory mixes your sales notes with your health plan with your client work.</p>\n<p>A team lets you keep each area small and focused. In a team setup, the bot that runs your website has no reason to see your finance inbox, and the bot that plans travel has no reason to touch your blog.</p>\n<p>That separation is the whole point.</p>\n<p>My own week runs like this: one bot for my site, one for social, one for my inbox, and none of them steps into the others' work.</p>\n<h3>The cost: setup</h3>\n<p>With one dot, there is nothing to design. With a team, you have to decide your areas and write down who owns what.</p>\n<p>That takes an afternoon, not a weekend, but it is real work.</p>\n\nSpend one afternoon listing your areas of work and writing down which bot owns each one.\n\n<h2>Control: rules vs scopes</h2>\n<p>Both products take safety seriously, but they put the control in different places.</p>\n<p><strong>Dots use Custom Rules.</strong> Each rule describes an action and sets whether the dot can do it alone, must ask first, or must never do it.</p>\n<p>Under that, an Auto-review layer checks actions that could affect your accounts or share information. Some tasks, like changing a password, always stay with you no matter what.</p>\n<p><strong>Grok Bot uses scopes plus approvals.</strong> You control what each Bot can reach by which connectors and logins you give it.</p>\n<p>Outgoing messages start as drafts you send yourself, and risky actions go through a safety check. Some steps, like a sign-in or a payment, are always handed back to you.</p>\n<figure>\n<p>Either way, an agent can only reach what you connect to it. In Grok Bot, each bot gets only the connectors you give it.</p>\n\nBefore you connect anything, list the connectors and logins each agent really needs, and give it only those.\n\n<h2>Recurring work: routines and skills</h2>\n<p>This is where the \"business in a monorepo\" crowd will feel at home.</p>\n<p>Grok Bot has two named building blocks:</p>\n<ul>\n<li>A <strong>skill</strong> is a saved recipe for a task, shared across your Bots.</li>\n<li>A <strong>routine</strong> tells one Bot when to run it, on a schedule or after an event like a Slack message or a GitHub notification.</li>\n</ul>\n<p>You can also show a Bot a workflow once and let it save the steps.</p>\n<p>Dots work toward goals you set and keep going in the background, and they learn from your feedback. OpenAI's launch material talks more about goals and ongoing work than about named, schedulable recipes.</p>\n<p>If your work is a set of repeatable weekly tasks, explicit routines are easier to inspect and debug. If your work is more open-ended, a goal-driven agent may need less setup.</p>\n\nPick one repeatable weekly task, show a Bot the workflow once, and let it save the steps as a skill.\n\n<h2>Where you talk to them</h2>\n<ul>\n<li><strong>Dots:</strong> ChatGPT on web, desktop and mobile, voice calls, plus Slack and Microsoft Teams. Texting is on a waitlist.</li>\n<li><strong>Grok Bot:</strong> a <a href=\"https://cursor.com/docs/grok-bot\">desktop app on macOS, Windows and Linux plus an iPhone app</a>, and group chats where several Bots work together.</li>\n</ul>\n<p>Dots also have an option to connect to your own laptop, which is off by default.</p>\n<h2>Access and availability</h2>\n<p>Dots are rolling out to ChatGPT Pro and Business Premium in eligible markets, with the first dot included in the plan.</p>\n<p>At launch, Pro in the EEA, Switzerland and the UK is not included, and Enterprise gets a beta that an admin must turn on. Pricing for extra dots has not been published yet.</p>\n<p>Grok Bot plans are listed on the <a href=\"https://x.ai/bot\">Grok Bot page</a>.</p>\n\nCheck both pricing pages before you decide, because both products are moving fast.\n\n<h2>Which one should you pick?</h2>\n<ul>\n<li><strong>Pick dots</strong> if you already live in ChatGPT, want one strong agent with almost no setup, and your tasks are varied rather than repeatable.</li>\n<li><strong>Pick Grok Bot</strong> if your work splits into clear areas, you want each area kept separate, and you like explicit routines you can read and change.</li>\n<li><strong>Try both on the same task</strong> if you are unsure.</li>\n</ul>\n\nGive each one boring job you do every week and compare the results after seven days.\n\n\n- Both products now give an agent its own cloud computer, background work and approvals.\n- Dots start as one personal agent; Grok Bot starts as a team of focused bots.\n- Dots control actions with Custom Rules; Grok Bot controls them with scopes, drafts and approvals.\n- Explicit routines and skills suit repeatable work; goal-driven agents suit open-ended work.\n- The best test is one real weekly task, run for a week.\n\n<p>Would you rather have one agent that knows everything about you, or a team where each one only knows its own job?</p>\n</figure></figure>",
      "date_published": "2026-10-03T00:00:00.000Z",
      "date_modified": "2026-10-03T00:00:00.000Z",
      "tags": [
        "ai",
        "ai",
        "agentic-ai",
        "ai-engineering",
        "security"
      ],
      "image": "https://ansezz.com/blog/grok-bot-vs-openai-dots/hero-0cfc216e.webp"
    },
    {
      "id": "https://ansezz.com/blog/run-your-business-with-ai-assistants/",
      "url": "https://ansezz.com/blog/run-your-business-with-ai-assistants/",
      "title": "Run your business with one AI assistant per area",
      "summary": "A simple setup for running any business with AI assistants: one hub, one bot per area of work, strict scopes, drafts before sends, and routines that stay quiet.",
      "content_html": "<p>By the time you know what actually needs you today, an hour is gone and your head is already tired. One assistant per area of work fixes most of that.</p>\n\n- Split your work into areas and give each one its own assistant, plus a hub that routes requests.\n- Use permissions, not prompts, to keep information apart.\n- Drafts before sends, approval before posting, and quiet unless something needs you.\n\n<h2>The morning problem</h2>\n<p>Most mornings used to start the same way.</p>\n\n  <div>\n<p>Open the inbox. Open the team chat. Open the other team chat. Check the calendar, the repo, the tickets, the notes app.</p>\n  </div>\n  <div>\n<p>Each area assistant watches its own inbox, sorts it and drafts replies. You only hear about it when something needs you.</p>\n  </div>\n\n<p>The problem is not that you have too many tools. The problem is that the context lives in all of them, and you are the only thing connecting them.</p>\n<p>Every switch costs focus, and things slip through the gaps between apps.</p>\n<p>For the last few weeks I have been running my work through Grok Bot with a setup that fixes most of this. Nothing here depends on what kind of business you run. If you have more than one area of work, you can copy it.</p>\n<h2>The core idea: one assistant per area</h2>\n<p>Do not build one giant assistant that knows everything. Split your work into areas, and give each area its own assistant.</p>\n<p>An area is anything with its own inbox, its own people, or its own rules. For most people it looks something like this:</p>\n<ul>\n<li><strong>Sales and leads:</strong> new inquiries, follow-ups, proposals.</li>\n<li><strong>Delivery:</strong> the actual work you ship, plus the tools it lives in.</li>\n<li><strong>Content:</strong> your website, blog and social posts.</li>\n<li><strong>Finance and admin:</strong> invoices, receipts, renewals.</li>\n<li><strong>Personal:</strong> travel, health, family logistics.</li>\n</ul>\n<h3>Add a hub on top</h3>\n<p>On top of these, add one <strong>hub</strong> assistant. The hub does no area work itself. It takes a request, decides which assistant owns it, and hands it over.</p>\n<p>When you are not sure where something belongs, you tell the hub and move on.</p>\n<figure>\n<h3>Why split at all?</h3>\n<p>Because each assistant stays small. It has a short list of tools, a clear job, and a memory that is only about its own area.</p>\n<p>It gets better at that job faster, and it is much harder for it to mix things up.</p>\n\nName your areas. Anything with its own inbox, its own people, or its own rules is an area.\n\n<h2>Give each assistant only its own keys</h2>\n<p>This is the most important rule, so it gets its own section.</p>\n<p>Each assistant connects only to the accounts its area needs. The content assistant gets the website repo and the design tool. The finance assistant gets the billing inbox. The delivery assistant gets the project tools.</p>\n<p>None of them get everything. This does two things for you:</p>\n<ol>\n<li><strong>It limits damage.</strong> If an assistant makes a mistake, it can only touch its own area.</li>\n<li><strong>It keeps information apart.</strong> Work from one area cannot leak into another, because the assistant simply has no access to it.</li>\n</ol>\n<p>You are not trusting a prompt to keep secrets. You are using permissions.</p>\n<p>If parts of your work must never mix, this matters even more. Give each one its own accounts and keep them fully separate.</p>\n\nFor each assistant, list the accounts its area needs and connect only those. Start with read access where you can.\n\n<h2>Guardrails that keep you in control</h2>\n<p>AI assistants are fast. That is the point, and also the risk. A few simple rules keep speed from turning into regret.</p>\n<p><strong>Write the scopes down.</strong> Keep one short file that says what each assistant owns, in one line each.</p>\n<p>When an assistant gets a request outside its scope, it hands it to the owner or the hub and says nothing more. No advice outside its lane.</p>\n<p><strong>Drafts before sends.</strong> Anything that goes out under your name, like an email, a message or a reply, starts as a draft you review. You press send. The only exceptions are the ones you set on purpose.</p>\n<p><strong>Approval before posting.</strong> Public content gets your OK before it goes live. Blog posts, social posts, anything with your name on it.</p>\n<p><strong>Quiet by default.</strong> Assistants only ping you when something needs a decision, a deadline is at risk, or something is blocked.</p>\n<p>No \"all clear\" messages, no empty digests. Silence means things are fine.</p>\n<p>These sound strict, and they are. Strict rules are what let you hand over more work without worrying about it.</p>\n\nWrite the scope file: one line per assistant saying what it owns. Share it with all of them.\n\n<h2>What a normal day looks like</h2>\n<p>Once the setup is running, the day changes shape.</p>\n<p><strong>Inbox checks run on their own.</strong> Each area assistant watches its own inbox on a schedule. It sorts, summarizes, and drafts replies. You only hear about it when something needs you.</p>\n<p><strong>Routines handle the repeat work.</strong> Weekly reports, content schedules, renewal reminders, the Monday \"what is open\" check.</p>\n<p>You write the routine once, in plain words, and it runs on its schedule. If a routine has nothing to report, it stays quiet.</p>\n<p><strong>Repeat tasks become saved skills.</strong> When you notice you are explaining the same steps a second time, save them as a skill. Next time, any assistant can follow the recipe without you walking it through again.</p>\n<p><strong>Assistants talk to each other.</strong> When the content assistant ships a new blog post, it tells the social assistant, which drafts the posts for your review. You do not carry the message between them.</p>\n<figure>\n<p>Your job shifts from doing every step to reviewing and deciding. That is where your time is worth the most anyway.</p>\n\nThe next time you explain the same steps a second time, save them as a skill.\n\n<h2>How to set it up for your own business</h2>\n<p>You do not need to build everything at once. In fact, please do not.</p>\n<ol>\n<li><strong>List your areas.</strong> Write down every inbox, chat and tool you check in a week. Group them into three to six areas.</li>\n<li><strong>Create one assistant per area.</strong> Give each a clear name and a one-line job description.</li>\n<li><strong>Connect only what each one needs.</strong> Start with read access where you can. Add write access once you trust it.</li>\n<li><strong>Write the scope file.</strong> One line per assistant. Share it with all of them.</li>\n<li><strong>Set the rules.</strong> Drafts before sends, approval before posting, quiet unless something needs you.</li>\n<li><strong>Add a hub last.</strong> Once you have two or three area assistants, the hub saves you from deciding who owns what.</li>\n<li><strong>Add routines slowly.</strong> One routine at a time. Watch it run for a week before you add the next.</li>\n</ol>\n<p>The order matters. People who start with ten routines and full access usually end up turning it all off after a bad week. People who start small keep going.</p>\n\nAdd one routine. Watch it run for a week before you add the next.\n\n<h2>Mistakes to avoid</h2>\n<table>\n<thead>\n<tr>\n<th>Mistake</th>\n<th>Why it hurts</th>\n<th>Do this instead</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>One assistant for everything</td>\n<td>It gets confused, and one wrong connection can expose all your work.</td>\n<td>One assistant per area.</td>\n</tr>\n<tr>\n<td>Too much access too early</td>\n<td>You hand over send or delete before trust is earned.</td>\n<td>Give read access first.</td>\n</tr>\n<tr>\n<td>Noisy assistants</td>\n<td>If every assistant pings you every hour, you have just built a new inbox.</td>\n<td>Set \"quiet unless needed\" from day one.</td>\n</tr>\n<tr>\n<td>No written scopes</td>\n<td>Assistants drift into each other's work, and you get duplicate or conflicting actions.</td>\n<td>Write the scope file.</td>\n</tr>\n<tr>\n<td>Skipping review</td>\n<td>The draft step feels slow at first.</td>\n<td>Keep it. It is still faster than fixing a message you cannot take back.</td>\n</tr>\n</tbody>\n</table>\n\n- Split your work into areas and give each one its own assistant.\n- Use permissions, not prompts, to keep information apart.\n- Drafts before sends, approval before posting, quiet by default.\n- Add a hub once you have a few assistants.\n- Start with one boring task you do every day, and hand that over first.\n\n<p>What is the one task you would hand over tomorrow if you knew it could not touch anything else?</p>\n</figure></figure>",
      "date_published": "2026-10-03T00:00:00.000Z",
      "date_modified": "2026-10-03T00:00:00.000Z",
      "tags": [
        "ai",
        "ai",
        "agentic-ai",
        "ai-engineering",
        "security"
      ],
      "image": "https://ansezz.com/blog/run-your-business-with-ai-assistants/hero-5b8df7af.webp"
    },
    {
      "id": "https://ansezz.com/blog/hitl-gates-for-agent-mutations/",
      "url": "https://ansezz.com/blog/hitl-gates-for-agent-mutations/",
      "title": "HITL gates for agent mutations",
      "summary": "Human-in-the-loop risk gates for refunds, inventory, and spend. Verify mutations after tool calls so agents cannot declare early victory.",
      "content_html": "<p>Agents are optimistic. They call a tool, see a JSON blob that looks success-shaped, and tell the user \"refund sent.\"</p>\n\n- Refunds, inventory writes, spend and credential changes need a human or a policy gate.\n- Approval ids come from your system, bound to actor, tenant, action and expiry. Never from the model.\n- After every mutation, a second read proves the world moved before anyone says \"done.\"\n\n<p>Sometimes the refund is pending. Sometimes the tool returned a structured error the model ignored. Sometimes the write never happened.</p>\n<p><strong>Human-in-the-loop (HITL) gates</strong> and <strong>post-mutation verification</strong> are how you stop early victory. The model can propose. Policy decides. The system checks reality before anyone celebrates.</p>\n<p>This is the mutation half of <a href=\"https://ansezz.com/blog/mcp-first-is-the-new-mobile-first/\">MCP first</a>: tools without judgment burn money.</p>\n<p>Pair with <a href=\"https://ansezz.com/blog/mcp-idempotency-laravel-mutations/\">idempotency</a>, <a href=\"https://ansezz.com/blog/mcp-structured-tool-errors-laravel/\">structured tool errors</a>, <a href=\"https://ansezz.com/blog/mcp-auth-audit-logging-laravel/\">auth and audit</a>, and commerce paths in <a href=\"https://ansezz.com/blog/secure-agentic-commerce-shopify/\">secure agentic commerce</a>.</p>\n<h2>Which mutations need a gate</h2>\n<p>Not every tool needs a human. Read-only lookups should be fast. Mutations that move money, stock, or trust should slow down.</p>\n<figure>\n<table>\n<thead>\n<tr>\n<th>Risk class</th>\n<th>Examples</th>\n<th>Default gate</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Money out</td>\n<td>Refunds, payouts, store credit</td>\n<td>Approve above threshold; auto only inside policy</td>\n</tr>\n<tr>\n<td>Inventory</td>\n<td>Force adjust, unpublish, bulk price</td>\n<td>Approve or dual-control for large deltas</td>\n</tr>\n<tr>\n<td>Spend / buy</td>\n<td>Agent checkout complete, ad spend</td>\n<td>Buyer confirm or spend ceiling</td>\n</tr>\n<tr>\n<td>Credentials</td>\n<td>Rotate keys, invite admins</td>\n<td>Always human for production</td>\n</tr>\n<tr>\n<td>Irreversible comms</td>\n<td>Mass email, legal notices</td>\n<td>Approve + preview</td>\n</tr>\n</tbody>\n</table>\n<p>Thresholds are product decisions. A small shipping adjustment is not the same as a large refund. Encode ceilings in policy config, not prompt text.</p>\n<p>For Shopify-style claims and refunds, the same idea shows up as queue triage with human resolution (<a href=\"https://ansezz.com/blog/shopify-claims-pipeline-claimify/\">claims pipeline</a>). Auto-clear the easy half; keep edge cases for people.</p>\n\nList every mutation tool you expose and tag each one with a risk class from the table above.\n\n<h2>Gate shapes that work</h2>\n<p>There are four shapes that hold up in production:</p>\n<ol>\n<li><strong>Pre-tool approval:</strong> agent prepares a <code>proposed_action</code> payload; UI or Slack asks a human to approve; only then does <code>execute_refund</code> run with the approval id.</li>\n<li><strong>Two-step tools:</strong> <code>draft_refund</code> (safe) then <code>commit_refund</code> (requires <code>approval_token</code>).</li>\n<li><strong>Async review queue:</strong> mutation creates a <code>pending</code> record; worker waits for approve/deny; agent polls status.</li>\n<li><strong>Policy auto-approve:</strong> small, low-risk, fully validated cases skip the human but still write audit + verification.</li>\n</ol>\n<h3>Bind every approval</h3>\n<p>Always bind approvals to:</p>\n<ul>\n<li>Actor (who asked)</li>\n<li>Tenant</li>\n<li>Exact action hash / idempotency key</li>\n<li>Expiry (stale approvals must not run tomorrow)</li>\n</ul>\n<p>Do not let the model invent an <code>approval_id</code>. Issue it from your system after a real human (or policy engine) decision.</p>\n<h2>Anti early-victory: verify after tools</h2>\n<p>The failure mode is consistent across stacks:</p>\n<ol>\n<li>Tool returns something the model interprets as success.</li>\n<li>Model narrates completion to the user.</li>\n<li>Downstream system never applied the change (validation, partial write, wrong environment).</li>\n</ol>\n\n  <div>The tool returns something success-shaped and the agent tells the user \"refund sent.\"</div>\n  <div>The system reads the refund back, confirms amount and status, then the agent says \"done.\"</div>\n\n<figure>\n<h3>What a second read looks like</h3>\n<p><strong>Verification</strong> is a second read (or event) that proves the world moved:</p>\n<ul>\n<li>After refund: fetch order transactions / refund set; confirm amount and status.</li>\n<li>After inventory adjust: read InventoryLevel (or your source of truth); confirm quantity.</li>\n<li>After spend: read ledger entry or checkout status; confirm <code>completed</code> not <code>open</code>.</li>\n<li>After key rotate: confirm old key rejected and new key works in a dry probe.</li>\n</ul>\n<p>Put verification in <strong>code</strong> the agent must call (or that your tool runner calls automatically), not only in a skill paragraph.</p>\n<p>Skills help sequencing; code enforces it.</p>\n<p>Pattern for a tool runner:</p>\n<pre><code>authorize -&gt; idempotency begin -&gt; mutate -&gt; verify -&gt; audit -&gt; respond\n</code></pre>\n<p>If verify fails, return a recoverable structured error (<code>verification_failed</code>) with what was expected vs observed. Do not return a soft \"ok\" with a warning buried in prose.</p>\n<h2>Where HITL sits in MCP / UCP</h2>\n<ul>\n<li><strong>Your SaaS MCP:</strong> gate inside the tool or a preceding approval tool; never expose bare Admin credentials to the model (<a href=\"https://ansezz.com/blog/secure-agentic-commerce-shopify/\">secure agentic commerce</a> for the commerce variant).</li>\n<li><strong>Shopify UCP checkout:</strong> <code>complete_checkout</code> already sits behind trust tiers and permissions (<a href=\"https://ansezz.com/blog/ucp-agent-profiles-are-the-new-oauth-app/\">agent profiles</a>, <a href=\"https://ansezz.com/blog/buyer-linked-tokens-agent-checkout/\">buyer-linked tokens</a>). Still verify order state before telling the buyer \"paid.\"</li>\n<li><strong>Laravel MCP:</strong> combine gates with <a href=\"https://ansezz.com/blog/mcp-idempotency-laravel-mutations/\">idempotency</a> so a double-approved click cannot double-refund.</li>\n</ul>\n<p>Stateless servers (<a href=\"https://ansezz.com/blog/stateless-mcp-servers-laravel/\">stateless MCP</a>) make this cleaner: approval records live in your DB, not in a vanished MCP session.</p>\n<h2>Implementation checklist</h2>\n<ol>\n<li>Classify tools: read / low-risk mutate / high-risk mutate.</li>\n<li>Define numeric ceilings per tenant for auto-approve.</li>\n<li>Require <code>approval_id</code> (system-issued) on high-risk commits.</li>\n<li>Expire approvals; bind them to idempotency keys.</li>\n<li>Auto-verify after mutate; fail closed on mismatch.</li>\n<li>Audit propose, approve, execute, verify as separate events.</li>\n<li>Teach skills: \"never claim success without verify tool result.\"</li>\n<li>UX: show the human the exact payload (amount, SKU, customer) before approve.</li>\n</ol>\n\nAdd an automatic verify step to your refund tool runner and make it fail closed on a mismatch.\n\n\n- HITL gates belong on refunds, inventory writes, spend, and credential changes.\n- Issue approval ids from your system; never trust model-invented tokens.\n- Verify mutations with a second read before user-facing success copy.\n- Encode ceilings and dual-control in policy config, not only prompts.\n- Audit propose, approve, execute and verify as distinct events.\n- Early victory is a product bug, not a charming agent quirk.\n\n<p>When your agent last said \"all done,\" did the ledger agree within a second, or did a human discover the gap in a support ticket two days later?</p>\n</figure></figure>",
      "date_published": "2026-10-02T00:00:00.000Z",
      "date_modified": "2026-10-02T00:00:00.000Z",
      "tags": [
        "architecture",
        "mcp",
        "agentic-ai",
        "security",
        "architecture",
        "ai",
        "laravel"
      ],
      "image": "https://ansezz.com/blog/hitl-gates-for-agent-mutations/hero-da1ef70c.webp"
    },
    {
      "id": "https://ansezz.com/blog/mcp-oauth-pkce-multi-tenant-saas/",
      "url": "https://ansezz.com/blog/mcp-oauth-pkce-multi-tenant-saas/",
      "title": "MCP OAuth PKCE for multi-tenant SaaS",
      "summary": "Laravel MCP requires PKCE S256 and prefers Client ID Metadata Documents. How multi-tenant SaaS should bind workspace at consent and avoid common agent auth failures.",
      "content_html": "<p>Agent OAuth fails in boring ways: missing PKCE, wrong <code>APP_URL</code>, a new dynamic client on every redirect, a token that has scopes but no tenant.</p>\n\n- Laravel MCP 1.0 requires PKCE S256 in your authorization server metadata before it will redirect.\n- Client ID Metadata Documents replace habitual Dynamic Client Registration, with a null client secret.\n- Scopes do not name a tenant. Bind the workspace at consent and enforce it on every tool call.\n\n<p>Laravel MCP 1.0 makes two of those failures explicit. As of the <a href=\"https://github.com/laravel/mcp/blob/main/UPGRADE.md\">1.0 upgrade guide</a> and <a href=\"https://laravel.com/docs/mcp\">Laravel MCP auth docs</a>:</p>\n<ol>\n<li><strong>PKCE is required.</strong> <code>OAuthClient::redirect()</code> throws if the authorization server metadata omits <code>code_challenge_methods_supported</code> (and S256 is the expected method).</li>\n<li><strong>Client ID Metadata Documents</strong> are preferred over Dynamic Client Registration (DCR). Your <code>client_id</code> can be an HTTPS URL to a JSON document your app hosts.</li>\n</ol>\n<p>This post is the multi-tenant SaaS reading of those rules: how to register clients, where tenant binding belongs, and the auth failures agents hit in production.</p>\n<p>Pair with <a href=\"https://ansezz.com/blog/stateless-mcp-servers-laravel/\">stateless MCP servers</a>, <a href=\"https://ansezz.com/blog/mcp-auth-audit-logging-laravel/\">MCP auth and audit</a>, and <a href=\"https://ansezz.com/blog/laravel-multi-tenancy/\">Laravel multi-tenancy</a>.</p>\n<h2>PKCE is not optional anymore</h2>\n<p>Previously, some servers omitted <code>code_challenge_methods_supported</code> and clients proceeded anyway. Laravel MCP 1.0 rejects that.</p>\n\n  <div>The server leaves out PKCE metadata and the client carries on anyway.</div>\n  <div>`redirect()` throws until the server advertises S256.</div>\n\n<p>If you operate the authorization server (Passport / your IdP), publish:</p>\n<pre><code>\"code_challenge_methods_supported\": [\"S256\"]\n</code></pre>\n<p>in <code>/.well-known/oauth-authorization-server</code>.</p>\n<p>If you are the MCP <strong>client</strong> connecting to a third party that will not advertise PKCE, you need another grant that fits (for example pre-issued credentials where appropriate). There is no \"turn off PKCE\" flag in <code>redirect()</code>.</p>\n<p>For public agents and desktop hosts, PKCE is how you survive without embedding a client secret. Treat missing PKCE metadata as a hard misconfiguration, not a soft warning.</p>\n\nOpen your `/.well-known/oauth-authorization-server` in a browser and confirm it lists `S256` under `code_challenge_methods_supported`.\n\n<h2>Client ID Metadata Documents</h2>\n<p>MCP revision <code>2026-07-28</code> deprecates DCR in favor of metadata documents. Flow in Laravel:</p>\n<ul>\n<li><code>Mcp::oAuthRoutesFor('github', $handler)</code> registers connect, callback, and <strong><code>GET /mcp/oauth/{client}/client-metadata.json</code></strong></li>\n<li>When the auth server advertises <code>client_id_metadata_document_supported</code>, Laravel can use that document URL as <code>client_id</code></li>\n<li>The app is treated as a <strong>public</strong> client: <code>token_endpoint_auth_method</code> is <code>none</code>, and <code>$token-&gt;clientSecret</code> is <code>null</code></li>\n</ul>\n<h3>What changes in your app</h3>\n<ul>\n<li>Database columns for <code>client_secret</code> must be <strong>nullable</strong></li>\n<li>Refresh / token calls must accept a null secret</li>\n<li><code>APP_URL</code> must be correct in production; the document is built from it, not from the incoming Host header</li>\n<li>The metadata route is <strong>unauthenticated</strong> on purpose so the authorization server can fetch it</li>\n</ul>\n<p>Custom path / extra fields:</p>\n<pre><code>Mcp::oAuthRoutesFor(\n    'github',\n    $handler,\n    clientMetadataUri: 'oauth/github/metadata.json',\n    clientMetadata: [\n        'client_name' =&gt; 'Acme Dashboard',\n        'logo_uri' =&gt; 'https://acme.com/logo.png',\n    ],\n);\n</code></pre>\n<p>This also fixed a nasty 0.x footgun: calling <code>redirect()</code> repeatedly could DCR a <strong>new</strong> client every time. Metadata documents stop that churn.</p>\n\nRun a migration that makes your `client_secret` column nullable before you switch to metadata documents.\n\n<h2>Multi-tenant: bind workspace at consent</h2>\n<p>Scopes alone do not name a tenant. A token with <code>mcp:use</code> (or your custom scopes) that is not bound to workspace <code>acme-eu</code> will happily call tools until your handler guesses wrong.</p>\n<figure>\n<p>Pattern that holds up:</p>\n<ol>\n<li><strong>Consent UI</strong> lets the human pick the workspace (or confirms the only one they can access).</li>\n<li>Persist <code>tenant_id</code> / <code>workspace_id</code> on the authorization code or token record you issue.</li>\n<li>On every tool call, resolve tenant from the <strong>token</strong>, not from a free-form argument the model invents.</li>\n<li>Optionally allow a tool argument to <em>select among authorized tenants</em>, never to invent a new one.</li>\n<li>Audit <code>user_id + tenant_id + tool + idempotency_key</code>.</li>\n</ol>\n<h3>Outbound tokens and audience</h3>\n<p>If you connect <strong>outbound</strong> as an MCP client to GitHub-like servers per tenant, store the TokenSet per <code>user_id + tenant_id + mcp_client_name</code>. Do not share one refresh across workspaces.</p>\n<p>Resource indicators / audience checks (RFC 8707 style) matter when the same IdP protects multiple APIs. Validate that the access token was minted for your MCP resource, not for an unrelated API.</p>\n<p>Resolve tenant from the token, not from a free-form argument the model invents.</p>\n<h2>Common agent auth failures</h2>\n<figure>\n<table>\n<thead>\n<tr>\n<th>Failure</th>\n<th>Symptom</th>\n<th>Fix</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>No PKCE advertisement</td>\n<td><code>OAuthException</code> on redirect</td>\n<td>Publish <code>code_challenge_methods_supported: [\"S256\"]</code></td>\n</tr>\n<tr>\n<td>Wrong <code>APP_URL</code></td>\n<td>Metadata <code>client_id</code> / redirect mismatch</td>\n<td>Fix env; redeploy metadata</td>\n</tr>\n<tr>\n<td>DCR every connect</td>\n<td>Orphan clients on IdP</td>\n<td>Prefer metadata documents</td>\n</tr>\n<tr>\n<td>Null secret surprise</td>\n<td>Insert fails / refresh breaks</td>\n<td>Nullable columns; auth method <code>none</code></td>\n</tr>\n<tr>\n<td>Scope without tenant</td>\n<td>Cross-tenant tool calls</td>\n<td>Bind tenant at consent; enforce in tools</td>\n</tr>\n<tr>\n<td>Legacy tests</td>\n<td>400 / <code>-32020</code></td>\n<td>Send MCP headers + <code>_meta</code> on 2026-07-28</td>\n</tr>\n</tbody>\n</table>\n<p>Agents will narrate these as \"the tool is broken.\" Your job is to make the HTTP and OAuth errors structured enough that the model (or your skill) can tell the human to reconnect.</p>\n<h2>Checklist for SaaS MCP OAuth</h2>\n<ol>\n<li>Advertise PKCE S256 on your auth server metadata.</li>\n<li>Prefer Client ID Metadata Documents; keep DCR only as fallback.</li>\n<li>Ensure metadata route is public; connect/callback stay behind <code>web</code> (or your chosen) middleware.</li>\n<li>Make <code>client_secret</code> storage nullable.</li>\n<li>Bind tenant at consent; enforce on every tool.</li>\n<li>Store outbound MCP tokens per tenant.</li>\n<li>Log auth failures with correlation ids (<a href=\"https://ansezz.com/blog/stateless-mcp-servers-laravel/\">stateless servers</a>).</li>\n<li>Dogfood with Claude / Cursor / your bot against a second workspace to prove isolation.</li>\n</ol>\n\n- Laravel MCP 1.0 requires PKCE support in authorization server metadata before redirect.\n- Client ID Metadata Documents replace habitual DCR; `client_id` may be a URL with null secret.\n- Multi-tenant safety is consent-time binding plus per-tool enforcement, not scope strings alone.\n- Fix `APP_URL`, nullable secrets, and header/`_meta` tests before blaming the model.\n- Treat reconnect UX as part of the product; agents cannot heal a broken OAuth client id.\n\n<p>If an agent can call <code>refund_order</code> with a token from workspace A while the chat is about workspace B, where did tenant binding fail: consent, token storage, or the tool handler?</p>\n</figure></figure>",
      "date_published": "2026-10-01T00:00:00.000Z",
      "date_modified": "2026-10-01T00:00:00.000Z",
      "tags": [
        "laravel",
        "laravel",
        "mcp",
        "security",
        "multi-tenancy",
        "ai",
        "agentic-ai"
      ],
      "image": "https://ansezz.com/blog/mcp-oauth-pkce-multi-tenant-saas/hero-699dfa1c.webp"
    },
    {
      "id": "https://ansezz.com/blog/stateless-mcp-servers-laravel/",
      "url": "https://ansezz.com/blog/stateless-mcp-servers-laravel/",
      "title": "Stateless MCP servers on Laravel",
      "summary": "Laravel MCP 1.0 drops session IDs. Scale with Octane and queues, keep auth and idempotency, and pass your own correlation ids in _meta.",
      "content_html": "<p>Laravel MCP 1.0 leans into protocol revision <code>2026-07-28</code>: <strong>every request stands alone</strong>.</p>\n\n- The `MCP-Session-Id` header and the session APIs are gone in Laravel MCP 1.0.\n- Scale with Octane and queues, and keep durable state in Redis or your database under your own ids.\n- Auth, tenant checks and idempotency now do the job the session used to do.\n\n<p>The <code>MCP-Session-Id</code> header is gone. <code>Request::sessionId()</code>, <code>setSessionId()</code>, and <code>SessionInitialized</code> are gone. If your tools cached \"conversation state\" under a session id, that design does not survive the upgrade.</p>\n<p>That sounds scary until you remember how you already scale Laravel: <strong>Octane workers</strong>, <strong>queues</strong>, and <strong>explicit keys</strong>. Stateless MCP is the same idea with stricter honesty.</p>\n<p>As of the <a href=\"https://github.com/laravel/mcp/blob/main/UPGRADE.md\">1.0 upgrade guide</a> and <a href=\"https://laravel.com/docs/mcp\">Laravel MCP docs</a>, modern requests carry protocol version and client capabilities in <code>params._meta</code>. Older <code>initialize</code> clients still negotiate, but new work should assume no server-side MCP session.</p>\n<h2>What you lose when sessions disappear</h2>\n<p>You lose a free place to stash:</p>\n<ul>\n<li>\"Last tool the user ran\"</li>\n<li>Multi-step wizard progress keyed only by MCP session</li>\n<li>Implicit auth context that was set once at initialize time</li>\n</ul>\n<p>You do <strong>not</strong> lose:</p>\n<ul>\n<li>Bearer / OAuth identity on each HTTP request</li>\n<li>Database rows, Redis keys, and queue jobs</li>\n<li>Idempotency keys you already should have had for mutations</li>\n</ul>\n<p>If a feature needed a session, give it a <strong>first-class id</strong> the client sends (workspace id, run id, approval id) in tool arguments or <code>_meta</code>.</p>\n\nSearch your code for `sessionId` and list every feature that depends on it. Each one needs its own explicit id.\n\n<h2>Scale with Octane and queues</h2>\n<figure>\n<p>HTTP MCP endpoints are ordinary Laravel routes (<code>Mcp::web</code>). Octane helps when you want warm workers and lower boot cost for chatty agents. Rules that already apply to Octane still apply:</p>\n<ul>\n<li>Do not leak mutable static state between requests</li>\n<li>Resolve tenant and auth <strong>per request</strong></li>\n<li>Prefer short tool handlers; push heavy work to queues</li>\n</ul>\n<h3>The long-running work pattern</h3>\n<ol>\n<li>Tool validates auth + input.</li>\n<li>Dispatch a job; return a structured <code>job_id</code> / status URL immediately.</li>\n<li>Store progress in Redis or the database.</li>\n<li>Optional follow-up tool <code>get_job_status</code> reads that store.</li>\n</ol>\n<p>That is more reliable than holding an SSE stream open for ten minutes of report generation, and it survives worker restarts. Pair with <a href=\"https://ansezz.com/blog/laravel-octane-high-traffic/\">Laravel Octane high traffic</a> notes if you are tuning the runtime.</p>\n<p>Streaming tools (generators + SSE on web servers) still work for progress notifications. Treat them as <strong>transport</strong>, not as durable session state.</p>\n\nTake your slowest tool, make it dispatch a job and return a `job_id`, and add a `get_job_status` tool that reads progress.\n\n<h2>Auth and idempotency become the session</h2>\n<p>Every mutation tool should already:</p>\n<ol>\n<li>Authenticate the actor (<code>auth:api</code> / Sanctum / your middleware).</li>\n<li>Authorize the tenant.</li>\n<li>Accept an <strong>idempotency key</strong> for refunds, rotates, and creates.</li>\n<li>Return <a href=\"https://ansezz.com/blog/mcp-structured-tool-errors-laravel/\">structured errors</a> the model can recover from.</li>\n<li>Write an <a href=\"https://ansezz.com/blog/mcp-auth-audit-logging-laravel/\">audit line</a>.</li>\n</ol>\n<p>Without MCP sessions, those five are not optional polish. They are how two Octane workers and a retried agent call stay coherent. See <a href=\"https://ansezz.com/blog/mcp-idempotency-laravel-mutations/\">MCP idempotency for Laravel mutations</a>.</p>\n<figure>\n<p>For protocol <code>2026-07-28</code> HTTP clients, also send matching <code>MCP-Protocol-Version</code>, <code>Mcp-Method</code>, and (for calls) <code>Mcp-Name</code> headers. Mismatches return HTTP 400 with JSON-RPC <code>-32020</code>. Update feature tests that used bare <code>postJson</code>.</p>\n<h2>Correlation without MCP sessions</h2>\n<p>Pass your own identifiers:</p>\n<pre><code>{\n  \"params\": {\n    \"name\": \"refund-order\",\n    \"arguments\": {\n      \"order_id\": \"gid://...\",\n      \"idempotency_key\": \"8f3c...\",\n      \"_meta\": {\n        \"io.modelcontextprotocol/protocolVersion\": \"2026-07-28\",\n        \"app.correlation_id\": \"run_01J...\"\n      }\n    }\n  }\n}\n</code></pre>\n<p>Log <code>app.correlation_id</code> (or your chosen key) beside tenant and user ids. Agents can resume by asking for status with that id. Humans can grep it in observability.</p>\n<p>Searchable catalogs and cache hints from Laravel MCP 1.0 (<a href=\"https://ansezz.com/blog/laravel-mcp-searchable-tool-catalogs/\">searchable tool catalogs</a>) also assume list/read responses may be cached while tool calls stay uncacheable.</p>\n<p>Stateless fits that model: discovery can be hot; mutations stay cold and keyed.</p>\n<h2>Migration checklist from 0.9 session code</h2>\n<ol>\n<li>Delete <code>sessionId</code> usage and <code>SessionInitialized</code> listeners.</li>\n<li>Move conversation memory to your app DB / Redis with explicit ids.</li>\n<li>Ensure every web MCP route still has auth middleware.</li>\n<li>Add idempotency on money and inventory tools if missing.</li>\n<li>Fix tests for <code>_meta</code> + MCP headers on <code>2026-07-28</code> clients.</li>\n<li>Load-test Octane with concurrent tool calls for the same tenant.</li>\n<li>Document for agent authors: \"no server session; send correlation + idempotency.\"</li>\n</ol>\n<h2>What \"stateless\" does not mean</h2>\n<p>Stateless MCP does not mean \"forget the user.\" It means the <strong>MCP transport</strong> does not own a sticky session bag between JSON-RPC messages.</p>\n\n  <div>The MCP session holds conversation state, wizard progress and auth context set once at initialize.</div>\n  <div>Each request carries identity. Your database carries history under ids you issued.</div>\n\n<p>Your product still has:</p>\n<ul>\n<li>Logged-in users and OAuth tokens</li>\n<li>Tenant databases and feature flags</li>\n<li>Conversation threads in <em>your</em> tables if you build a chat product</li>\n<li>Approval records waiting for a human</li>\n</ul>\n<p>Those live in application storage. The MCP server becomes a thin, horizontally scalable door into that storage. That is the same split you want for <a href=\"https://ansezz.com/blog/laravel-multi-tenancy/\">multi-tenant Laravel</a> APIs.</p>\n<p>The request carries identity; the database carries history.</p>\n<p>When an agent \"resumes\" work, it should call a tool with the ids you issued earlier (<code>run_id</code>, <code>draft_id</code>, <code>approval_id</code>), not hope the same load balancer owns an in-memory map from last Tuesday.</p>\n\n- Laravel MCP 1.0 processes each request independently; MCP session APIs are removed.\n- Scale with Octane and queues; keep durable state in Redis/DB under your own ids.\n- Auth, tenant checks, and idempotency replace \"remembered\" session context.\n- Modern calls carry protocol + capabilities in `params._meta` (and matching HTTP headers).\n- Streaming is fine for progress; it is not a substitute for durable job state.\n\n<p>If you deleted <code>MCP-Session-Id</code> tomorrow, which of your tools would still be safe under two parallel Octane workers retrying the same agent turn?</p>\n</figure></figure>",
      "date_published": "2026-09-30T00:00:00.000Z",
      "date_modified": "2026-09-30T00:00:00.000Z",
      "tags": [
        "laravel",
        "laravel",
        "mcp",
        "architecture",
        "ai",
        "agentic-ai",
        "performance"
      ],
      "image": "https://ansezz.com/blog/stateless-mcp-servers-laravel/hero-26121bcc.webp"
    },
    {
      "id": "https://ansezz.com/blog/buyer-linked-tokens-agent-checkout/",
      "url": "https://ansezz.com/blog/buyer-linked-tokens-agent-checkout/",
      "title": "Buyer-linked tokens for agent checkout",
      "summary": "Exchange a Shop session for a Shopify buyer-linked JWT (~60 minutes, no refresh) so signed-in agents can personalize catalog and complete checkout.",
      "content_html": "<p>A Token-tier agent credential identifies <strong>your app</strong>. A <strong>buyer-linked token</strong> also carries the signed-in Shop customer.</p>\n\n- A buyer-linked token turns \"an agent is shopping\" into \"this buyer is shopping through an agent.\"\n- You get it in three server-side hops: Shop OAuth, token exchange for a JWT grant, then a redeem at Shopify.\n- The grant lives about 60 seconds. The token lives about 60 minutes and never refreshes, so plan to remint.\n\n<p>As of Sep 2026, Shopify documents a UCP delegated identity provider flow: authorize with Shop (<code>accounts.shop.app</code>), exchange the Shop access token for a short-lived JWT grant, redeem that grant at Shopify for a buyer-linked JWT.</p>\n<p>The resulting token lasts about <strong>60 minutes</strong> and <strong>does not refresh</strong>. When it expires, remint from the Shop session.</p>\n<p>Official reference: <a href=\"https://shopify.dev/docs/agents/profiles/buyer-linked-tokens\">Create a buyer-linked token</a>. Pair with <a href=\"https://ansezz.com/blog/ucp-agent-profiles-are-the-new-oauth-app/\">UCP agent profiles</a>, <a href=\"https://shopify.dev/docs/agents/profiles/auth-and-rate-limiting\">auth and rate limiting</a>, and <a href=\"https://ansezz.com/blog/secure-agentic-commerce-shopify/\">secure agentic commerce</a>.</p>\n<h2>Why buyer linkage matters</h2>\n<p>With a buyer-linked token you can:</p>\n<ul>\n<li>Return <strong>personalized</strong> Global Catalog search results</li>\n<li>Sign the buyer into a <strong>merchant store</strong> checkout path</li>\n<li>Apply automatic discounts that depend on the signed-in customer</li>\n<li>Operate at Token-tier limits with purchase scopes when granted (for example <code>dev.ucp.shopping.checkout:manage</code>)</li>\n</ul>\n<p>Without it, you may still build carts anonymously or with an app-only token, but you miss buyer-aware behavior and you still need the right permission story for <code>complete_checkout</code>.</p>\n\n  <div>An agent is shopping. Carts work, but nothing knows who the buyer is.</div>\n  <div>This buyer is shopping through an agent, with personalized results and signed-in checkout.</div>\n\n<p>Think of it as identity linking for agentic checkout, not as a forever API key.</p>\n<h2>The three-step chain (server-side)</h2>\n<figure>\n<p><strong>Confidential client secret stays on a server you control.</strong> Public clients cannot complete this flow safely. Do not ship the secret in browser or mobile binaries.</p>\n<p>Discover endpoints at runtime (do not hard-code forever):</p>\n<ol>\n<li>Resource server <code>/.well-known/oauth-protected-resource</code> → Shopify authorization server</li>\n<li>Shopify UCP business profile → <code>dev.ucp.common.identity_linking</code> → Shop as delegated IdP</li>\n<li>Shop <code>/.well-known/oauth-authorization-server</code> → authorize + token endpoints</li>\n</ol>\n<p>Then run the three steps below.</p>\n<h3>1. Authorize the customer with Shop</h3>\n<p>Standard OAuth authorization code against Shop. Reuse Dev Dashboard client id/secret and redirect URI.</p>\n<p>Request every scope you plan to redeem later (include checkout manage if you will call checkout tools).</p>\n<p>You receive a Shop <strong>access_token</strong> (example responses show <code>expires_in: 3600</code>).</p>\n<h3>2. Exchange at Shop for a JWT grant</h3>\n<p>Token exchange (RFC 8693) at Shop's token endpoint:</p>\n<ul>\n<li><code>grant_type=urn:ietf:params:oauth:grant-type:token-exchange</code></li>\n<li><code>subject_token</code> = Shop access token</li>\n<li><code>requested_token_type</code> = JWT</li>\n<li><code>audience</code> = <code>api.shopify.com</code> (Global Catalog) or the <strong>merchant store domain</strong> for store checkout</li>\n</ul>\n<p>The returned <code>access_token</code> is a <strong>JWT authorization grant</strong>: signed, <strong>single-use</strong>, about <strong>60 seconds</strong> to live. It is not your API bearer yet. To inspect its claims while you debug, paste a test token into my <a href=\"https://ansezz.com/tools/jwt-decoder/\">JWT decoder</a>.</p>\n<h3>3. Redeem at Shopify for the buyer-linked token</h3>\n<p>JWT bearer assertion (RFC 7523) at Shopify's (or the store's) token endpoint with the scopes you need. Shopify verifies the grant against Shop's keys, resolves the customer, and issues the buyer-linked JWT.</p>\n<p>For merchant checkout, discovery points at the store's authorization server. Set audience to the store domain and request scopes such as <code>openid</code> and <code>dev.ucp.shopping.checkout:manage</code> as documented for signed-in checkout tools.</p>\n\nWrite down which `audience` each of your flows needs (Global Catalog or a store domain) before you write any exchange code.\n\n<h2>TTL reality: 60s grant, ~60 min token, no refresh</h2>\n<figure>\n<p>Two clocks, often confused:</p>\n<table>\n<thead>\n<tr>\n<th>Artifact</th>\n<th>Lifetime (documented)</th>\n<th>Refresh?</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>JWT authorization grant from Shop</td>\n<td>~60 seconds, single-use</td>\n<td>No; re-exchange if needed</td>\n</tr>\n<tr>\n<td>Buyer-linked token from Shopify</td>\n<td>~60 minutes</td>\n<td><strong>No refresh token</strong>; remint via the chain</td>\n</tr>\n</tbody>\n</table>\n<p>Build session handling around reminting, not around <code>refresh_token</code> muscle memory from classic OAuth apps. Cache the buyer-linked JWT only within its lifetime, keyed to the buyer and resource server, on the server.</p>\n<p>Build session handling around reminting, not around refresh_token muscle memory.</p>\n<p>If <code>complete_checkout</code> still fails with a Token-tier client, check purchase permission and merchant/agent eligibility separately. Community threads show scope alone is not always sufficient; treat platform policy as part of the design.</p>\n\nAdd a remint step that runs a few minutes before the 60 minute mark, instead of waiting for a 401.\n\n<h2>Implementation checklist</h2>\n<ol>\n<li>Keep client secret server-side only.</li>\n<li>Discover auth URLs from well-known metadata; honor cache headers.</li>\n<li>OAuth the buyer to Shop with the scopes you will redeem.</li>\n<li>Exchange with the correct <code>audience</code> (global vs store).</li>\n<li>Redeem promptly before the 60s grant expires.</li>\n<li>Send the buyer-linked JWT as Bearer on MCP calls that need buyer context.</li>\n<li>Remint near expiry; do not expect a refresh token.</li>\n<li>Pair with a real <a href=\"https://ansezz.com/blog/ucp-agent-profiles-are-the-new-oauth-app/\">agent profile</a> and the right trust tier expectations.</li>\n<li>Prefer Cart MCP for browsing; Checkout MCP when committing (<a href=\"https://ansezz.com/blog/migrate-storefront-mcp-cart-to-ucp/\">cart migration notes</a>).</li>\n</ol>\n<h2>Failure modes to expect</h2>\n<ul>\n<li><strong>Grant expired or reused:</strong> exchange again; grants are single-use and short.</li>\n<li><strong>Wrong audience:</strong> catalog grant redeemed at a store (or the reverse).</li>\n<li><strong>Browser exchange:</strong> secret leakage; move the flow to your backend.</li>\n<li><strong>Missing checkout scope:</strong> token works for search but not manage checkout.</li>\n<li><strong>Assuming refresh:</strong> sessions die at ~60 minutes unless you remint.</li>\n</ul>\n<p>Log correlation ids and HTTP status from each hop. Do not ask the model to \"retry checkout\" when the token is simply dead.</p>\n\n- Buyer-linked tokens bind a Shop customer to your agent credential for personalized and signed-in commerce.\n- Flow: Shop OAuth, then token exchange (JWT grant), then Shopify JWT bearer redeem.\n- Run exchange and redeem only on a confidential server.\n- Grant ~60s single-use; buyer-linked token ~60 minutes with **no refresh**.\n- Discover endpoints; set audience to global catalog or merchant store deliberately.\n- Remint from the Shop session; design UX for re-link, not silent forever tokens.\n\n<p>When your agent's checkout fails at minute 61, is the model confused about tools, or did you forget that buyer-linked JWTs die without a refresh token?</p>\n</figure></figure>",
      "date_published": "2026-09-29T00:00:00.000Z",
      "date_modified": "2026-09-29T00:00:00.000Z",
      "tags": [
        "shopify",
        "shopify",
        "agentic-commerce",
        "security",
        "mcp",
        "ai",
        "agentic-ai"
      ],
      "image": "https://ansezz.com/blog/buyer-linked-tokens-agent-checkout/hero-bda22a7c.webp"
    },
    {
      "id": "https://ansezz.com/blog/ucp-agent-profiles-are-the-new-oauth-app/",
      "url": "https://ansezz.com/blog/ucp-agent-profiles-are-the-new-oauth-app/",
      "title": "UCP agent profiles are the new OAuth app",
      "summary": "Host a UCP agent profile URL, declare cart and checkout capabilities, and climb Anonymous, Signed, and Token trust tiers for Shopify MCP.",
      "content_html": "<p>In the OAuth decade you registered an app and got a client id. In agentic commerce you host a <strong>UCP agent profile</strong>: a public JSON document at a URL you control.</p>\n\n- Every Shopify Cart MCP and Checkout MCP call asks for your agent profile URL.\n- Declare cart and checkout together if you want to turn a cart into a checkout.\n- Anonymous, Signed and Token trust tiers decide your rate limits and whether `complete_checkout` is possible.\n\n\n  <div>Register an app, get a client id, and merchants decide whether to trust you.</div>\n  <div>Host a profile URL. Merchants fetch it, negotiate capabilities and check your keys.</div>\n\n<p>Every Cart MCP and Checkout MCP call on Shopify asks for that URL in <code>meta.ucp-agent.profile</code>. The merchant fetches it, negotiates capabilities, and (for signed traffic) checks your published keys.</p>\n<p>No profile, no serious cart/checkout conversation.</p>\n<p>This post covers what to put in the profile, why cart and checkout belong together, and how Anonymous / Signed / Token tiers change what you can do.</p>\n<p>Cross-read <a href=\"https://ansezz.com/blog/shopify-ucp-quick-start/\">Shopify UCP quick-start</a>, <a href=\"https://ansezz.com/blog/secure-agentic-commerce-shopify/\">secure agentic commerce</a>, and the <a href=\"https://ansezz.com/blog/migrate-storefront-mcp-cart-to-ucp/\">Storefront MCP cart → UCP migration</a>.</p>\n<p>Official guides: <a href=\"https://shopify.dev/docs/agents/get-started/profile\">Define your agent profile</a> and <a href=\"https://shopify.dev/docs/agents/profiles/auth-and-rate-limiting\">Auth and rate limiting</a>.</p>\n<p>Version pins below are as of Sep 2026; check them against the current docs.</p>\n<h2>What the profile is</h2>\n<p>An agent profile is a JSON document that declares:</p>\n<ul>\n<li>The UCP version your agent speaks (examples in Shopify docs use <code>2026-08-25</code>)</li>\n<li>The capabilities you support, with versions</li>\n</ul>\n<p>Minimal cart + checkout profile (from Shopify's published examples):</p>\n<pre><code>{\n  \"ucp\": {\n    \"version\": \"2026-08-25\",\n    \"capabilities\": {\n      \"dev.ucp.shopping.cart\": [{ \"version\": \"2026-08-25\" }],\n      \"dev.ucp.shopping.checkout\": [{ \"version\": \"2026-08-25\" }]\n    }\n  }\n}\n</code></pre>\n<h3>Where to host it</h3>\n<p>Host it somewhere stable and HTTPS:</p>\n<ul>\n<li>Static host (<code>https://your-app.example/profiles/agent.json</code>)</li>\n<li>Or a well-known path on your domain</li>\n</ul>\n<p>Then send that URL on every MCP cart/checkout request. Merchants treat it like app metadata: fetch, cache per directives, negotiate intersection of capabilities.</p>\n<p>For signed requests, the profile also carries the public key material used to verify HTTP Message Signatures (RFC 9421 / ECDSA P-256 per UCP). That is the \"app credentials\" half of the story without a browser consent screen on every call.</p>\n\nPublish the minimal profile above on a stable HTTPS URL, then fetch it with `curl` from outside your network to confirm a merchant could read it.\n\n<h2>Why cart and checkout both matter</h2>\n<figure>\n<p>Passing a <code>cart_id</code> into <code>create_checkout</code> requires <strong>both</strong> capabilities to negotiate successfully. A cart-only profile is fine for browse-and-share (<code>continue_url</code>). It is not enough for the purchase handoff.</p>\n<p>Declare what you actually implement. Inflating capabilities you cannot fulfill is how you fail negotiation or fail later in checkout.</p>\n<p>Under-declaring is how you debug \"why won't create_checkout accept my cart\" for an afternoon.</p>\n<p>Keep exploratory line-item work on Cart MCP; open Checkout MCP when the buyer is ready. Rate limits are stricter on checkout at every trust tier.</p>\n<h2>Trust tiers are the new permission story</h2>\n<figure>\n<p>Shopify classifies UCP MCP traffic by how the agent identifies itself (as of the Sep 2026 auth docs):</p>\n<table>\n<thead>\n<tr>\n<th>Tier</th>\n<th>How you identify</th>\n<th>Catalog / cart / checkout build</th>\n<th><code>complete_checkout</code></th>\n<th>Relative limits</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Anonymous</strong></td>\n<td>No auth or signature</td>\n<td>Yes</td>\n<td>No</td>\n<td>Lowest</td>\n</tr>\n<tr>\n<td><strong>Signed</strong></td>\n<td>HTTP Message Signatures; key in profile</td>\n<td>Yes</td>\n<td>No</td>\n<td>Mid</td>\n</tr>\n<tr>\n<td><strong>Token</strong></td>\n<td>Bearer JWT (Dev Dashboard / customer / shop / buyer-linked)</td>\n<td>Yes (buyer-linked can personalize)</td>\n<td>When purchase permission is granted</td>\n<td>Highest</td>\n</tr>\n</tbody>\n</table>\n<p>Order tools (<code>get_order</code>) sit on the Token tier with the right scopes, and only for orders placed through your agent.</p>\n<h3>Reading the tiers</h3>\n<ol>\n<li><strong>Anonymous</strong> is for demos and early browse. Expect tight throttling.</li>\n<li><strong>Signed</strong> proves which agent you are without a full OAuth dance on every request. Still cannot complete checkout.</li>\n<li><strong>Token</strong> is where purchase completion and stronger limits live. Buyer-linked tokens additionally carry the Shop customer identity for personalized catalog and signed-in checkout behavior.</li>\n</ol>\n<p>Climb tiers on purpose. Do not ask for <code>complete_checkout</code> from an anonymous client and treat the 401 as a model failure.</p>\n\nWrite down which tier your agent runs on today, and what it would take to reach the next one.\n\n<h2>Profile hygiene checklist</h2>\n<ol>\n<li>Host one canonical HTTPS URL; avoid rotating paths weekly.</li>\n<li>Declare only capabilities you implement; pin versions you tested.</li>\n<li>Include cart <strong>and</strong> checkout if you convert carts.</li>\n<li>Publish signing keys if you use the Signed tier; rotate with overlap.</li>\n<li>Pass <code>meta.ucp-agent.profile</code> on every cart/checkout tool call.</li>\n<li>Prefer Cart MCP for iteration; Checkout MCP for commit.</li>\n<li>Move to Token (and buyer-linked when needed) before promising one-click complete.</li>\n<li>Keep secrets off the profile document. Public keys yes; client secrets never.</li>\n</ol>\n<p>Think of the profile as your OAuth app listing plus capability manifest.</p>\n<p>Merchants will judge your profile by what it claims and what your traffic tier can actually do.</p>\n<h2>Relation to MCP-first product design</h2>\n<p>If you also host your own MCP servers (Laravel or otherwise), you already know the pain of fat tool lists and weak auth.</p>\n<p>UCP profiles are the commerce-protocol cousin of that discipline: <strong>advertise operable capabilities</strong>, then back them with auth that matches risk.</p>\n<p>See <a href=\"https://ansezz.com/blog/mcp-first-is-the-new-mobile-first/\">MCP first</a> and <a href=\"https://ansezz.com/blog/api-vs-mcp/\">API vs MCP</a> for the product-side framing; this post is the Shopify UCP instantiation.</p>\n\n- The UCP agent profile URL is how merchants negotiate who you are and what you can do.\n- Declare `dev.ucp.shopping.cart` and `dev.ucp.shopping.checkout` together when you convert carts.\n- Anonymous, Signed, and Token tiers gate rate limits and whether `complete_checkout` is even on the table.\n- Signed proves agent identity via profile keys; Token is required for purchase completion permissions.\n- Keep the profile stable, versioned, and honest. Capability theater fails in negotiation or at checkout.\n\n<p>If you shipped a shopping agent tomorrow, would your public profile URL survive a merchant fetch right now, and which trust tier are you actually operating on when you say \"we support checkout\"?</p>\n</figure></figure>",
      "date_published": "2026-09-28T00:00:00.000Z",
      "date_modified": "2026-09-28T00:00:00.000Z",
      "tags": [
        "shopify",
        "shopify",
        "agentic-commerce",
        "mcp",
        "security",
        "ai",
        "agentic-ai"
      ],
      "image": "https://ansezz.com/blog/ucp-agent-profiles-are-the-new-oauth-app/hero-98fc8bbc.webp"
    },
    {
      "id": "https://ansezz.com/blog/commerce-rag-is-not-document-rag/",
      "url": "https://ansezz.com/blog/commerce-rag-is-not-document-rag/",
      "title": "Commerce RAG is not document RAG",
      "summary": "Shopify Plus catalog RAG needs hybrid lexical plus vector search, then commerce ranking on stock, velocity, and margin. Not LangChain on PDFs.",
      "content_html": "<p>If your \"catalog AI\" is a PDF tutorial wearing a Shopify hoodie, you will ship confident answers that recommend out-of-stock, low-margin, or dead SKUs.</p>\n\n- Document RAG optimizes for passage relevance. Commerce RAG has to optimize for sellable, profitable products.\n- Retrieve with hybrid lexical plus vector search, then rank on live stock, velocity, margin and merchandising rules.\n- Keep price and inventory out of the embedding. Read them live at query time.\n\n<p>Document RAG optimizes for <strong>passage relevance</strong>. Commerce RAG optimizes for <strong>sellable, profitable, in-policy products</strong>. Same family of techniques. Different system design.</p>\n<p>This post is the cut line for Shopify Plus (and any large catalog) work: hybrid lexical + vector retrieval, then a <strong>commerce ranking</strong> layer that uses live stock, velocity, margin, and merchandising rules.</p>\n<p>Pair it with <a href=\"https://ansezz.com/blog/why-your-rag-is-failing/\">why your RAG is failing</a>, <a href=\"https://ansezz.com/blog/7-rag-mistakes-production/\">7 RAG mistakes in production</a>, and agent-facing catalog tools via <a href=\"https://ansezz.com/blog/shopify-ucp-quick-start/\">Shopify UCP quick-start</a>.</p>\n<h2>Document RAG assumed a handbook</h2>\n<p>Classic demo loop:</p>\n<ol>\n<li>Chunk a PDF or help center.</li>\n<li>Embed chunks once.</li>\n<li>Retrieve top-k by cosine similarity.</li>\n<li>Stuff passages into the prompt.</li>\n<li>Generate an answer.</li>\n</ol>\n<p>That works when the truth is mostly static prose. Policies, runbooks, warranty text.</p>\n<p>It fails when the \"document\" is a <strong>product graph</strong> whose price, inventory, and variants change every minute.</p>\n<p>A product is not a paragraph.</p>\n<p>It is title, options, collections, metafields, media, and a set of <strong>volatile facts</strong> that should never be baked into the only retrieval score.</p>\n<h2>What catalog RAG actually has to do</h2>\n<p>For storefront search, chat commerce, and agent tools (including UCP <code>search_catalog</code> / <code>get_product</code> on <code>/api/ucp/mcp</code>), the job is closer to:</p>\n<ol>\n<li><strong>Understand intent</strong> (gift under $50, waterproof trail shoe, restock of last order).</li>\n<li><strong>Retrieve candidates</strong> that match language <em>and</em> identifiers (SKU, barcode, vendor code).</li>\n<li><strong>Filter</strong> to what can actually sell in this market right now.</li>\n<li><strong>Rank</strong> by business outcomes, not only semantic score.</li>\n<li><strong>Ground the agent</strong> with live fields before it speaks price or availability.</li>\n</ol>\n<p>Shopify's Storefront Catalog MCP gives agents a search surface. It does not replace your ranking philosophy for a custom Plus app, Hydrogen search, or internal shopping agent.</p>\n<p>Treat MCP as a channel. Own retrieval quality behind it when you build custom.</p>\n<h2>Stage 1: hybrid lexical + vector</h2>\n<p>Pure vector search misses exact SKUs, model numbers, and brand strings the embedding smoothed away. Pure BM25 / keyword search misses \"shoes for wide feet on wet rock\" when the title says \"trail runner.\"</p>\n<p><strong>Hybrid retrieval</strong> runs both and fuses ranks (reciprocal rank fusion or a tuned weighted blend). That is the same medicine as general production RAG (<a href=\"https://ansezz.com/blog/why-your-rag-is-failing/\">why your RAG is failing</a>), applied to product text fields: title, description, tags, option names, curated metafields.</p>\n<figure>\n<p>Practical embedding content:</p>\n<ul>\n<li>Prefer stable descriptive text and attributes.</li>\n<li>Keep identifiers searchable in the lexical index.</li>\n<li>Re-embed on content changes (product update webhooks), not on every inventory tick.</li>\n</ul>\n<p>If you are on pgvector in Laravel, the hybrid pattern still holds: SQL <code>tsvector</code> / external search engine for lexical, ANN for semantic, fuse, then rank.</p>\n<p>Stack choice is secondary to the split. See <a href=\"https://ansezz.com/blog/picking-the-right-rag-stack/\">picking the right RAG stack</a> when you are still selecting stores.</p>\n\nSearch your catalog assistant for an exact SKU and for a plain-language need. If one of them fails, you need the other half of hybrid retrieval.\n\n<h2>Stage 2: commerce ranking is not optional</h2>\n<p>Retrieval returns a candidate set. Commerce ranking decides what a buyer or agent should see first.</p>\n<p>Signals that belong <strong>after</strong> hybrid retrieve (illustrative, tune per vertical):</p>\n<table>\n<thead>\n<tr>\n<th>Signal</th>\n<th>Why it matters</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>In-stock / location available</td>\n<td>Semantic winners that cannot ship are false friends</td>\n</tr>\n<tr>\n<td>Sales velocity (7/30-day)</td>\n<td>Preference for what actually converts</td>\n</tr>\n<tr>\n<td>Margin / contribution</td>\n<td>Ambiguous queries should not default to junk economics</td>\n</tr>\n<tr>\n<td>Return rate</td>\n<td>High semantic fit with high returns hurts trust</td>\n</tr>\n<tr>\n<td>Campaign pins / suppressions</td>\n<td>Merchandising is policy, not cosine</td>\n</tr>\n<tr>\n<td>Newness / overstock</td>\n<td>Launch windows and aging inventory need soft boosts</td>\n</tr>\n</tbody>\n</table>\n<p>A product that is \"perfectly similar\" and converts poorly is a bad recommendation. The embedding does not know that unless you engineer the signal into ranking (or into a learned reranker that consumes those features).</p>\n<p>Do not pretend one cosine score encodes margin. It does not.</p>\n<h2>Keep volatile fields out of the vector</h2>\n<figure>\n<p>Common failure mode: concatenate <code>\"price: $49 in stock: 3 margin: high\"</code> into the embedding text. Overnight a sale and a sell-through change the truth; the vector still argues yesterday.</p>\n\n  <div>Price, stock and margin are concatenated into the embedding text, and the vector still argues yesterday.</div>\n  <div>The index holds descriptive content. Price and stock come in at query time. Margin and velocity feed the ranker.</div>\n\n<p>Better split:</p>\n<ul>\n<li><strong>In the index:</strong> descriptive and categorical content you are willing to re-embed on product edits.</li>\n<li><strong>At query time:</strong> price, inventory, availability by market, active discounts.</li>\n<li><strong>In the ranker:</strong> margin, velocity, return rate, pins, from a fast store updated by Admin/webhooks or ShopifyQL-style metrics exports.</li>\n</ul>\n<p>Inventory especially: listen to inventory webhooks (or poll InventoryLevel), keep a KV / Redis / table of live quantities, <strong>filter or demote</strong> after retrieval. Regenerating embeddings on every stock change is expensive and still races reality.</p>\n<p>This is the ecommerce version of \"don't RAG what should be an API call.\" Policies and size guides can live in document RAG. \"How many left in EU warehouse?\" should not.</p>\n\nGrep your embedding text builder for price, stock and margin fields, and move each one to a live read.\n\n<h2>Agents make the gap louder</h2>\n<p>A human scanning a PLP can mentally skip sold-out cards. An agent that trusts top-k embeddings will propose them unless you enforce availability and policy in the tool layer.</p>\n<p>For agentic commerce:</p>\n<ul>\n<li>Ground answers with <code>get_product</code> (or Admin/Storefront live reads) before stating price or stock.</li>\n<li>Prefer catalog tools that already filter <code>available</code> when the Shopify catalog extension is in play; still verify before checkout.</li>\n<li>Put ranking rules in code and skills, not only in prompt text. Prompts drift; filters do not.</li>\n<li>Secure the path: agents should not hold broad Admin credentials. See <a href=\"https://ansezz.com/blog/secure-agentic-commerce-shopify/\">secure agentic commerce on Shopify</a>.</li>\n</ul>\n<p>Agent channels (UCP / MCP) amplify whatever retrieval quality you already have. They do not invent merchandising judgment.</p>\n<h2>A minimal architecture checklist</h2>\n<ol>\n<li>Separate <strong>document corpora</strong> (policies, FAQs) from <strong>catalog indices</strong>.</li>\n<li>Hybrid retrieve on catalog text; fuse lexical + vector.</li>\n<li>Filter with <strong>live</strong> stock and market availability.</li>\n<li>Rank with velocity, margin, returns, and campaign rules.</li>\n<li>Re-embed on content webhooks; never on pure inventory ticks.</li>\n<li>Eval with commerce metrics (purchase rate, regret, stock-miss rate), not only nDCG on title similarity.</li>\n<li>For chat/agents, require a live product read before money language.</li>\n</ol>\n<p>You do not need a research lab to start. You need the humility to stop treating SKUs like PDF chunks.</p>\n\n- Document RAG optimizes passage relevance; commerce RAG must optimize sellable outcomes.\n- Hybrid lexical + vector retrieval catches SKUs and semantic intent; neither alone is enough at catalog scale.\n- Ranking on stock, velocity, margin, and merchandising rules comes **after** retrieval.\n- Keep price and inventory out of embeddings; read them live.\n- Agents will confidently recommend bad stock unless tools and filters enforce reality.\n- Evaluate with commerce outcomes, not only embedding similarity demos.\n\n<p>If your catalog assistant still chunks product CSVs like a handbook, which false recommendation would hurt more this week: an out-of-stock hero SKU, or an in-stock low-margin substitute that crowds out the product you actually wanted to sell?</p>\n</figure></figure>",
      "date_published": "2026-09-27T00:00:00.000Z",
      "date_modified": "2026-09-27T00:00:00.000Z",
      "tags": [
        "shopify",
        "shopify",
        "rag",
        "ai",
        "llm",
        "agentic-commerce",
        "pgvector"
      ],
      "image": "https://ansezz.com/blog/commerce-rag-is-not-document-rag/hero-ea0d7b7c.webp"
    },
    {
      "id": "https://ansezz.com/blog/laravel-mcp-searchable-tool-catalogs/",
      "url": "https://ansezz.com/blog/laravel-mcp-searchable-tool-catalogs/",
      "title": "Laravel MCP searchable tool catalogs",
      "summary": "Laravel MCP 1.0 searchable catalogs and cache hints cut context bloat. When to expose tools versus discover them with search_tools and execute_tools.",
      "content_html": "<p>Every tool you advertise to a model costs tokens, up front.</p>\n\n- A fat `tools/list` taxes the context window. Keep a few hot tools and park the long tail behind `ToolSearch`.\n- Cache hints cut how often clients refetch discovery and resources. Tool calls are never cached.\n- Default to private cache scope for tenant-filtered catalogs, and keep auth and idempotency on every mutation.\n\n<p>Names, descriptions, input schemas, output schemas. A SaaS MCP server with forty admin actions can burn a surprising slice of the context window before the user asks a single question.</p>\n<p>Laravel MCP 1.0 (as of the stable release covering protocol revision <code>2026-07-28</code>) gives you two levers for that problem: <strong>searchable tool catalogs</strong> and <strong>cache hints</strong>. One shrinks what the model sees. The other reduces how often clients re-fetch discovery and resource payloads.</p>\n<p>This post is the practical split: which tools stay in the hot list, which go behind <code>ToolSearch</code>, and how to set <code>ttlMs</code> / scope without lying to shared caches.</p>\n<p>Pair it with the <a href=\"https://ansezz.com/blog/mcp-first-is-the-new-mobile-first/\">MCP first</a> framing and the <a href=\"https://ansezz.com/blog/mcp-auth-audit-logging-laravel/\">Laravel MCP safety trilogy</a> when you wire real mutations. Official docs: <a href=\"https://laravel.com/docs/mcp\">Laravel MCP</a> (Searchable Tool Catalogs and Cache Hints sections).</p>\n<h2>The context tax of a fat tools/list</h2>\n<p>When a client calls <code>tools/list</code>, every returned tool definition becomes candidate context for the model. That is fine for five crisp tools. It hurts when you expose:</p>\n<ul>\n<li>Rare seasonal ops (<code>close_year_end_books</code>)</li>\n<li>Deep admin toggles nobody asks for in chat</li>\n<li>Large schemas with nested enums and long descriptions</li>\n<li>Tenant-specific tools that only matter after a search</li>\n</ul>\n<p>The agent does not need those definitions on turn one. It needs a short path to <strong>find</strong> them when the user asks.</p>\n<p>Laravel MCP's answer is <code>ToolSearch</code>: keep hot tools in <code>$tools</code> as normal classes, and park the long tail under a <code>ToolSearch::class =&gt; [...]</code> entry.</p>\n<h2>How ToolSearch works in Laravel MCP 1.0</h2>\n<p>As documented, a searchable catalog registers two meta-tools for the client:</p>\n<ul>\n<li><code>search_tools</code>: query by name, description, and input schema; returns matches with names, descriptions, and expected inputs (respect a result limit)</li>\n<li><code>execute_tools</code>: invoke one or more tools by name from what search (or prior knowledge) surfaced</li>\n</ul>\n<p>Minimal server shape:</p>\n<pre><code>use Laravel\\Mcp\\Server;\nuse Laravel\\Mcp\\Server\\Tools\\ToolSearch;\n\nclass OpsServer extends Server\n{\n    protected array $tools = [\n        // Always advertised...\n        CreateInvoiceTool::class,\n        ListDelayedOrdersTool::class,\n\n        // Discovered on demand...\n        ToolSearch::class =&gt; [\n            HistoricalAuditTool::class,\n            RotateApiKeyTool::class,\n            ExportComplianceCsvTool::class,\n        ],\n    ];\n}\n</code></pre>\n<p><code>CurrentWeatherTool</code>-style examples in the docs make the same point: advertise the common case; search for historical weather and alerts.</p>\n<h3>Limits and conditional registration</h3>\n<p>Limits for batched execution live in config:</p>\n<ul>\n<li><code>mcp.tool_search.max_tool_calls</code></li>\n<li><code>mcp.tool_search.max_output_bytes</code></li>\n</ul>\n<p>Tune those before an agent can fan out ten heavy tools in one <code>execute_tools</code> call.</p>\n<p><strong>Conditional registration still applies.</strong> If a catalog tool's <code>shouldRegister</code> returns <code>false</code>, it will not appear in search results and cannot be executed. That matters for plan-gated or tenant-gated features.</p>\n\nSet `mcp.tool_search.max_tool_calls` and `mcp.tool_search.max_output_bytes` in config before you move any tool behind `ToolSearch`.\n\n<h2>When to expose vs discover</h2>\n<figure>\n<p>Use this as a decision table, not a vibe:</p>\n<table>\n<thead>\n<tr>\n<th>Put it in the hot list when...</th>\n<th>Put it behind ToolSearch when...</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>It is the main outcome users ask for</td>\n<td>It is long-tail or seasonal</td>\n</tr>\n<tr>\n<td>Search wording is ambiguous (\"fix order\" could mean five tools)</td>\n<td>The name and description search cleanly</td>\n</tr>\n<tr>\n<td>You need zero discovery latency</td>\n<td>An extra search turn is acceptable</td>\n</tr>\n<tr>\n<td>It is the safe entry point into a skill</td>\n<td>It is a deep follow-up after triage</td>\n</tr>\n<tr>\n<td>Schema is small and stable</td>\n<td>Schema is large or rarely needed</td>\n</tr>\n</tbody>\n</table>\n<p>Examples from a multi-tenant ops server:</p>\n<ul>\n<li><strong>Expose:</strong> <code>list_delayed_orders</code>, <code>get_order</code>, <code>draft_customer_message</code></li>\n<li><strong>Discover:</strong> <code>issue_partial_refund</code> (after policy skill), <code>export_tax_csv</code>, <code>rotate_webhook_secret</code></li>\n</ul>\n<p>Exposing every mutation \"so the model can see it\" is how you get eager refunds. Discoverability plus a skill that says <em>when</em> to search is safer.</p>\n<p>Tools without judgment burn money.</p>\n<p>That is the same discipline as <a href=\"https://ansezz.com/blog/mcp-first-is-the-new-mobile-first/\">MCP first</a>.</p>\n<p>If your agent already struggles with wrong tool choice, searchable catalogs alone will not fix bad descriptions. Write <code>#[Description]</code> like a product brief. Ambiguous copy poisons both the hot list and search rankings.</p>\n<h2>Cache hints: stop refetching discovery</h2>\n<figure>\n<p>Laravel MCP attaches <strong>advisory</strong> cache hints to responses that may be cached: server discovery, primitive listings, and resource reads. Defaults are conservative: private scope, <code>ttlMs</code> of zero (do not cache).</p>\n<p>You can set a server default with <code>#[Cacheable]</code> and override per method via <code>cacheHints()</code>:</p>\n<pre><code>use Laravel\\Mcp\\Enums\\CacheScope;\nuse Laravel\\Mcp\\Server\\Attributes\\Cacheable;\n\n#[Cacheable(ttlMs: 60_000, scope: CacheScope::Public)]\nclass OpsServer extends Server\n{\n    protected function cacheHints(): array\n    {\n        return [\n            'tools/list' =&gt; new Cacheable(\n                ttlMs: 30_000,\n                scope: CacheScope::Public,\n            ),\n        ];\n    }\n}\n</code></pre>\n<h3>Rules of thumb</h3>\n<p>From the docs and MCP caching draft:</p>\n<ul>\n<li><strong><code>CacheScope::Public</code></strong>: safe to share across users (same tool list for everyone)</li>\n<li><strong><code>CacheScope::Private</code></strong>: tied to the authorization context (filtered lists, user-specific resources)</li>\n<li><strong>Resource-level <code>#[Cacheable]</code></strong> wins over method and server defaults</li>\n<li><strong>Missing or zero <code>ttlMs</code></strong> means do not cache</li>\n<li><strong>Tool calls are never cacheable</strong> (mutations and live reads stay fresh)</li>\n</ul>\n<p>On the client side, Laravel's MCP client honors hints when you opt in with <code>withCache()</code>. Installing 1.0 does not enable caching by itself.</p>\n<p>That is good: accidental public caching of a private <code>tools/list</code> is a tenancy bug waiting to happen.</p>\n\n  <div>Plan features change the tool list per shop, but `tools/list` is cached as public because the route is authenticated.</div>\n  <div>Tenant-filtered lists stay private. Public scope only when every caller sees the same primitives.</div>\n\n<p>For multi-tenant SaaS, default private unless you are sure every authenticated caller sees the same primitives. If plan features change the tool list per shop, public scope is wrong even if the HTTP route is authenticated.</p>\n\nCall `tools/list` as two users on different plans. If the lists differ, make sure its cache scope is private.\n\n<h2>A practical split for a Laravel SaaS MCP</h2>\n<p>Here is a pattern that holds up on real products:</p>\n<ol>\n<li><strong>Three to seven hot tools</strong> that cover 80% of chat intents.</li>\n<li><strong>Everything else under <code>ToolSearch</code></strong>, with descriptions that include synonyms users actually say.</li>\n<li><strong>Skills / playbooks</strong> that tell the agent to <code>search_tools</code> before inventing a workflow.</li>\n<li><strong>Public short TTL</strong> on <code>tools/list</code> only when the catalog is identical for all callers.</li>\n<li><strong>Private longer TTL</strong> on stable docs resources; zero TTL on anything tenant-flavored.</li>\n<li><strong>Auth, idempotency, structured errors</strong> on mutations (<a href=\"https://ansezz.com/blog/mcp-auth-audit-logging-laravel/\">auth and audit</a>, <a href=\"https://ansezz.com/blog/mcp-idempotency-laravel-mutations/\">idempotency</a>, <a href=\"https://ansezz.com/blog/mcp-structured-tool-errors-laravel/\">tool errors</a>).</li>\n</ol>\n<p>Searchable catalogs reduce tokens. They do not replace permission checks. An agent that finds <code>rotate_api_key</code> via search still needs the same actor, tenant, and audit line as a hot tool.</p>\n<p>If you are connecting agents as MCP clients rather than only hosting servers, the Laravel AI SDK path of spreading <code>Client::web(...)-&gt;tools()</code> into an agent still benefits: fewer advertised tools means less noise in the agent loop.</p>\n<p>Cache the listed collection when it is stable, as the AI SDK docs suggest for remote OAuth servers.</p>\n<h2>What this is not</h2>\n<ul>\n<li><strong>Not</strong> \"hide unsafe tools and hope search never finds them.\" <code>shouldRegister</code> and authorization still decide availability.</li>\n<li><strong>Not</strong> a substitute for pagination and concise schemas on the tools you do expose.</li>\n<li><strong>Not</strong> a reason to mark tenant-specific lists <code>Public</code> for a cheap cache hit.</li>\n<li><strong>Not</strong> automatic speed: hints are advisory; clients must opt in.</li>\n</ul>\n<p>Protocol note: Laravel MCP 1.0 speaks <code>2026-07-28</code> (including <code>server/discover</code>) while still negotiating older <code>initialize</code> clients. If your host is mid-upgrade, check the <a href=\"https://laravel.com/docs/mcp\">Laravel MCP docs</a> for your installed package version.</p>\n\n- Fat `tools/list` payloads tax the context window before the user speaks.\n- Laravel MCP 1.0 `ToolSearch` keeps hot tools advertised and parks the long tail behind `search_tools` / `execute_tools`.\n- Expose high-frequency, ambiguous, or entry-point tools; discover long-tail and heavy-schema tools.\n- Cache hints (`ttlMs`, public/private) apply to discovery and resource reads, not tool calls.\n- Prefer private scope for tenant-filtered catalogs; public only when every caller sees the same list.\n- Pair catalogs with skills, auth, and idempotency. Discovery without governance is just a shorter path to the wrong mutation.\n\n<p>If you listed every tool your admin UI exposes into MCP tomorrow, which five would earn a permanent slot in the hot list, and which ones should stay invisible until <code>search_tools</code> earns them?</p>\n</figure></figure>",
      "date_published": "2026-09-26T00:00:00.000Z",
      "date_modified": "2026-09-26T00:00:00.000Z",
      "tags": [
        "laravel",
        "laravel",
        "mcp",
        "agentic-ai",
        "ai",
        "architecture",
        "claude"
      ],
      "image": "https://ansezz.com/blog/laravel-mcp-searchable-tool-catalogs/hero-a9c8079c.webp"
    },
    {
      "id": "https://ansezz.com/blog/migrate-storefront-mcp-cart-to-ucp/",
      "url": "https://ansezz.com/blog/migrate-storefront-mcp-cart-to-ucp/",
      "title": "Migrate Storefront MCP carts to UCP Cart MCP",
      "summary": "Storefront MCP get_cart and update_cart on /api/mcp are deprecated. Move agents to UCP Cart MCP at /api/ucp/mcp with create, get, update, and cancel.",
      "content_html": "<p>If your shopping agent still calls <code>get_cart</code> and <code>update_cart</code> on <code>https://{shop}.myshopify.com/api/mcp</code>, you are running on deprecated tools.</p>\n\n- Storefront MCP cart tools on `/api/mcp` lost maintenance on August 31, 2026. Move to UCP Cart MCP at `/api/ucp/mcp`.\n- UCP gives you four tools: `create_cart`, `get_cart`, `update_cart` and `cancel_cart`, and every call needs your agent profile.\n- `update_cart` replaces the whole cart. Send the full state every time or you will empty carts.\n\n<p>Shopify announced the deprecation on June 24, 2026. Maintenance for those Storefront MCP cart tools ended on <strong>August 31, 2026</strong>.</p>\n<p>Agents that have not moved are now on unmaintained tools, and they can fail mid-conversation while buyers still expect a cart.</p>\n<p>The replacement is <strong>UCP Cart MCP</strong> at <code>https://{shop-domain}/api/ucp/mcp</code>. Same commerce job. Different endpoint, tool set, and request shape.</p>\n<p>This post is the migration map: what breaks, what replaces it, and a checklist you can run against any agent still wired to <code>/api/mcp</code>.</p>\n<p>For the broader agent-ready storefront story, start with the <a href=\"https://ansezz.com/blog/shopify-ucp-quick-start/\">Shopify UCP quick-start</a> and <a href=\"https://ansezz.com/blog/secure-agentic-commerce-shopify/\">secure agentic commerce on Shopify</a>.</p>\n<h2>What is being deprecated</h2>\n<p>On Storefront MCP (<code>/api/mcp</code>), cart support was essentially two tools:</p>\n<ul>\n<li><code>get_cart</code></li>\n<li><code>update_cart</code></li>\n</ul>\n<p>Those tools mixed \"create if missing\" behavior into update, patched line items like a classic AJAX cart, and lived next to policy search on the legacy storefront MCP surface.</p>\n<p>UCP Cart MCP implements the UCP cart capability (<code>dev.ucp.shopping.cart</code>, version <code>2026-08-25</code> in current Shopify docs) and exposes four tools on <code>/api/ucp/mcp</code>:</p>\n<table>\n<thead>\n<tr>\n<th>Tool</th>\n<th>Job</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>create_cart</code></td>\n<td>Start a cart with line items and optional buyer context</td>\n</tr>\n<tr>\n<td><code>get_cart</code></td>\n<td>Read current cart state and estimated totals</td>\n</tr>\n<tr>\n<td><code>update_cart</code></td>\n<td><strong>Replace</strong> the full cart state (PUT semantics)</td>\n</tr>\n<tr>\n<td><code>cancel_cart</code></td>\n<td>Delete the cart; requires an idempotency key</td>\n</tr>\n</tbody>\n</table>\n<p>Catalog discovery (<code>search_catalog</code>, <code>lookup_catalog</code>, <code>get_product</code>) already moved toward the UCP endpoint. Cart is catching up. Treat <code>/api/mcp</code> cart calls as a compatibility shim, not a long-term contract.</p>\n<p>Official references: <a href=\"https://shopify.dev/changelog/storefront-mcp-cart-tools-are-being-deprecated-in-favour-of-ucp-cart-mcp\">deprecation changelog</a> and <a href=\"https://shopify.dev/docs/agents/carts-and-checkout/cart-mcp\">Cart MCP docs</a>.</p>\n<h2>Endpoint and request shape</h2>\n<p>Point every cart <code>tools/call</code> at:</p>\n<pre><code>POST https://{shop-domain}/api/ucp/mcp\n</code></pre>\n<p>Cart tools accept <strong>unauthenticated</strong> requests. That lets an agent estimate totals and share a <code>continue_url</code> before the buyer signs in.</p>\n<p>Checkout is a different server with stricter auth and rate limits; keep exploratory work on Cart MCP.</p>\n<h3>The meta block</h3>\n<p>Every cart call must include <code>meta</code> in <code>arguments</code>:</p>\n<pre><code>{\n  \"meta\": {\n    \"ucp-agent\": {\n      \"profile\": \"https://your-agent.example/.well-known/ucp\"\n    }\n  }\n}\n</code></pre>\n<p>That profile URI is how the merchant negotiates capabilities. Skipping it is not a soft warning; it is a failed request.</p>\n<p>Hosting that profile is the agent equivalent of registering an OAuth app. Pair this post with the secure agentic commerce guidance when you harden trust tiers.</p>\n<h3>Ids and responses</h3>\n<p>For <code>get_cart</code>, <code>update_cart</code>, and <code>cancel_cart</code>, pass the cart id as a <strong>top-level</strong> <code>id</code> in <code>arguments</code>. Do not nest <code>id</code> inside the <code>cart</code> object on write.</p>\n<p>Responses land in <code>result.structuredContent</code>. Read the <code>cart</code> object there.</p>\n<p>Business problems (expired cart, quantity adjusted) often arrive as a successful JSON-RPC <code>result</code> with entries in <code>cart.messages</code>, not as a protocol <code>error</code>. If your client only checks JSON-RPC errors, you will miss quantity adjustments and <code>not_found</code>.</p>\n\nAdd a check in your tool wrapper that reads `structuredContent.cart.messages` on every response, even when JSON-RPC reports success.\n\n<h2>create_cart is not optional</h2>\n<p>Legacy <code>update_cart</code> often created a cart when no id was sent. UCP splits that out.</p>\n<p>Use <code>create_cart</code> when the buyer has selected variants and you want estimated totals, multi-turn edits, or a shareable <code>continue_url</code> without starting checkout. Include:</p>\n<ul>\n<li><code>cart.line_items[]</code> with <code>quantity</code> and <code>item.id</code> (variant GID, which my <a href=\"https://ansezz.com/tools/shopify-gid-decoder/\">Shopify GID decoder</a> can unpack)</li>\n<li>optional <code>context</code> (<code>address_country</code>, region, postal code) for pricing and availability hints</li>\n<li>optional <code>attribution</code> (UTM / click ids) if you will later convert to checkout</li>\n<li>optional <code>buyer</code> for personalized estimates</li>\n</ul>\n<p>If you plan to call Checkout MCP <code>create_checkout</code> with this cart, set attribution on the cart <strong>before</strong> conversion. The merchant uses the cart's line items, context, buyer, and attribution when creating checkout and ignores overlapping fields in the checkout payload.</p>\n<p>When the buyer is ready to pay, pass the cart <code>id</code> as <code>cart_id</code> into <code>create_checkout</code>. Conversion is idempotent for the same cart id.</p>\n<h2>PUT semantics will break naive agents</h2>\n<p>This is the sharp edge of the migration.</p>\n<p><code>update_cart</code> on UCP Cart MCP <strong>replaces</strong> the cart. Omit <code>line_items</code>, <code>context</code>, or <code>attribution</code> and those fields are removed. There is no server-side merge.</p>\n<p>That differs from Storefront API and AJAX cart mutations, and from the old Storefront MCP patch-style mental model.</p>\n\n  <div>Send only the field that changed and let the server merge it.</div>\n  <div>Send the entire desired cart, including `attribution` and `context`, on every update.</div>\n\n<figure>\n<p>Practical rules for agent tool wrappers:</p>\n<ol>\n<li>Keep the last known full cart snapshot in your session store.</li>\n<li>Apply the buyer's delta in your code (qty change, remove line, new postal code).</li>\n<li>Send the <strong>entire</strong> desired <code>cart</code> object on every <code>update_cart</code>.</li>\n<li>Resend <code>attribution</code> and <code>context</code> if you still want them.</li>\n</ol>\n<p>If you teach the model \"just send the field that changed,\" you will empty carts in production.</p>\n<p>Put that rule in the tool description and in your skill playbook, not only in a human README.</p>\n<h2>cancel_cart needs an idempotency key</h2>\n<p><code>cancel_cart</code> requires <code>meta[\"idempotency-key\"]</code> as a <a href=\"https://ansezz.com/tools/uuid-generator/\">UUID</a> in addition to <code>ucp-agent.profile</code>. Cancel removes the cart from storage. Later <code>get_cart</code> returns a business <code>not_found</code> outcome.</p>\n<p>Use cancel when the buyer abandons, or when you need to clear a stale cart before starting clean.</p>\n<p>Retries without a stable idempotency key are how you get confusing double-cancel paths. The same discipline shows up in <a href=\"https://ansezz.com/blog/mcp-idempotency-laravel-mutations/\">MCP idempotency for Laravel mutations</a> even when the server is Shopify's, not yours.</p>\n<h2>Cart vs checkout: keep exploration cheap</h2>\n<p>Carts are long-lived browsing containers. Checkouts are short-lived purchase sessions with stricter freshness and rate limits.</p>\n<table>\n<thead>\n<tr>\n<th>Use case</th>\n<th>Prefer</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Multi-turn browsing and total estimates</td>\n<td>Cart MCP</td>\n</tr>\n<tr>\n<td>Share a link with the buyer</td>\n<td>Cart MCP <code>continue_url</code></td>\n</tr>\n<tr>\n<td>Buyer ready to purchase</td>\n<td>Checkout MCP</td>\n</tr>\n<tr>\n<td>Complete payment in your app</td>\n<td>Checkout MCP</td>\n</tr>\n</tbody>\n</table>\n<p>Rate limits scale with how the agent identifies itself (Bearer, signed, anonymous). Anonymous Cart MCP works for exploration; Checkout MCP does not. Keep chatty quantity tweaks off checkout tools.</p>\n<p>This split matches the <a href=\"https://ansezz.com/blog/mcp-vs-a2a-vs-acp/\">MCP vs A2A vs ACP</a> idea: pick the protocol surface that matches the job, not the one your first tutorial used.</p>\n<h2>Migration checklist for agents on /api/mcp</h2>\n<figure>\n<p>Run this against every agent, skill, and integration that still mentions Storefront MCP carts:</p>\n<ol>\n<li><strong>Find callers.</strong> Search for <code>/api/mcp</code>, <code>get_cart</code>, and <code>update_cart</code> in agent configs, MCP client URLs, and tool wrappers.</li>\n<li><strong>Retarget the endpoint</strong> to <code>/api/ucp/mcp</code> for cart tools.</li>\n<li><strong>Add <code>create_cart</code></strong> as an explicit step; stop inventing carts inside update.</li>\n<li><strong>Require <code>meta.ucp-agent.profile</code></strong> on every cart call.</li>\n<li><strong>Store and resend full cart state</strong> on <code>update_cart</code> (PUT semantics).</li>\n<li><strong>Pass cart id as top-level <code>id</code></strong> for get, update, and cancel.</li>\n<li><strong>Add UUID <code>idempotency-key</code></strong> for <code>cancel_cart</code>.</li>\n<li><strong>Parse <code>structuredContent.cart.messages</code></strong> for business outcomes (<code>not_found</code>, <code>quantity_adjusted</code>, and friends).</li>\n<li><strong>Wire checkout separately:</strong> cart id into <code>create_checkout</code> only when the buyer commits.</li>\n<li><strong>Dogfood on staging before you cut over.</strong> Hit a staging shop with create → update (full replace) → get → cancel → create_checkout.</li>\n</ol>\n<p>If you are still designing the product around agents rather than bolting MCP onto a human-only flow, the <a href=\"https://ansezz.com/blog/mcp-first-is-the-new-mobile-first/\">MCP first</a> framing helps: ship the cart tools and skills as the capability contract, then let the chat UI sit on top.</p>\n\nStart with step 1 today: search your agent configs and tool wrappers for `/api/mcp` and list every hit.\n\n\n- Storefront MCP `get_cart` / `update_cart` on `/api/mcp` are deprecated; maintenance ended August 31, 2026. Migrate now.\n- Migrate cart traffic to UCP Cart MCP at `/api/ucp/mcp` with `create_cart`, `get_cart`, `update_cart`, and `cancel_cart`.\n- Every request needs `meta.ucp-agent.profile`; cancel also needs a UUID idempotency key.\n- `update_cart` is full replacement. Partial payloads wipe omitted fields.\n- Keep browsing on Cart MCP; move to Checkout MCP only when the buyer is ready to pay.\n- Treat `cart.messages` as first-class outcomes, not optional log noise.\n\n<p>If your agent still hard-codes <code>/api/mcp</code> for carts, which step fails first in staging: missing agent profile, empty cart after a partial update, or a checkout call that never saw a real cart id?</p>\n</figure></figure>",
      "date_published": "2026-09-25T00:00:00.000Z",
      "date_modified": "2026-09-25T00:00:00.000Z",
      "tags": [
        "shopify",
        "shopify",
        "agentic-commerce",
        "mcp",
        "agentic-ai",
        "ai"
      ],
      "image": "https://ansezz.com/blog/migrate-storefront-mcp-cart-to-ucp/hero-0958b96c.webp"
    },
    {
      "id": "https://ansezz.com/blog/shopify-claims-pipeline-claimify/",
      "url": "https://ansezz.com/blog/shopify-claims-pipeline-claimify/",
      "title": "One Shopify claims pipeline for every claim type",
      "summary": "A returns policy is not a returns process. Build one Shopify claims pipeline for returns, damage, and warranty: file, eligibility, queue, Shopify resolve.",
      "content_html": "<p>Your returns policy PDF is not a returns process.</p>\n\n- One pipeline can carry returns, damage claims and warranty, as long as the claim type decides rules, evidence and resolutions.\n- A claim counts as done only after a refund, replacement or credit lands in Shopify with an audit record.\n- Let rules handle the routine claims so your team spends its time on the hard ones.\n\n<p>A customer emails a blurry photo of a cracked box. Support digs through orders to confirm the purchase. Someone promises a refund in Slack.</p>\n<p>Three days later the same claim is still \"waiting on photos,\" and nobody can say who owns it. That is not a claims system. That is an inbox wearing a policy hat.</p>\n<p>I built <a href=\"https://apps.shopify.com/claimify\">Claimify</a> because I kept seeing the same pattern on Shopify stores: every claim type sprawling into its own spreadsheet, helpdesk macro, or ad-hoc Google Form. Returns in one place. Damage in another. Warranty buried in a shared mailbox.</p>\n<p>The fix is not a prettier form. It is one end-to-end pipeline that every claim type walks through, from the moment a customer files to the moment Shopify actually refunds, replaces, or credits.</p>\n<h2>Policy is not process</h2>\n<p>A policy answers <em>what</em> you allow. A process answers <em>how</em> it moves.</p>\n\n  <div>\"You have 30 days to return unused items.\"</div>\n  <div>The customer files from their order, eligibility checks the fulfilment date and product tags, the claim lands in a queue with an owner, photos are required when the type needs them, and resolution writes back to Shopify with an audit trail.</div>\n\n<p>If you only publish the policy, every exception becomes a negotiation in email. If you only build a form without states, you get a ticket graveyard.</p>\n<p>The pipeline is the missing middle: typed claim in, typed resolution out, with explicit transitions in between.</p>\n<h2>What \"any claim type\" actually means</h2>\n<p>\"Claims\" is not a synonym for returns. On a real storefront you usually need at least:</p>\n<ul>\n<li><strong>Returns and exchanges (RMA)</strong> with tracking states: requested, label issued, in transit, received, inspected, resolved.</li>\n<li><strong>Damage on arrival</strong> with photo requirements up front, not after three reply-all threads.</li>\n<li><strong>Warranty</strong> as a first-class lifecycle: policies, registration, serials, coverage windows. Not a renamed return form with the word \"warranty\" in the title.</li>\n<li><strong>Refund and goodwill paths</strong> that are still claims, even when no product comes back.</li>\n</ul>\n<p>One queue can hold all of them if the type is a field on the claim, not a separate product.</p>\n<p>Claim type drives which eligibility rules run, which evidence is required, and which resolutions are legal for that SKU. That is the difference between a generic \"support form\" and a claims system.</p>\n\nList every place a claim can arrive today (email, chat, forms, DMs). Each one is a queue nobody owns.\n\n<h2>Customer file to merchant resolve</h2>\n<figure>\n<p>The loop should be boring and complete.</p>\n<p><strong>Customer side.</strong> File from the account page, from an order action, or from a theme app block on a help page. Pick the order line, pick the claim type, describe the issue, attach evidence. No copying order numbers into a contact form that pretend-matches later.</p>\n<p><strong>Merchant side.</strong> The claim opens in Shopify Admin with live order context: line items, fulfilment dates, customer tags, previous claims on the same order.</p>\n<p>Assign an owner. Move it through a Kanban (new, review, resolve) or list view. Comment with history that does not vanish when someone leaves the company.</p>\n<p><strong>Integrations.</strong> Status changes should fire webhooks and talk to the tools you already run: Shopify Flow, Gorgias, Klaviyo, Zapier, Make. The helpdesk does not need to <em>be</em> the system of record. It needs to stay in sync with it.</p>\n<p>If a claim can die in a thread without a state change, you do not have a pipeline. You have chat.</p>\n<h2>Warranty is its own lifecycle</h2>\n<p>Warranty is where most \"claims apps\" quietly fail. They slap a warranty label on a return flow and call it done.</p>\n<p>That breaks the moment you need registration, a serial number, a coverage window that starts at fulfilment (or at first activation), or a different resolution set than a 30-day return.</p>\n<p>Treat warranty as a product surface:</p>\n<ol>\n<li><strong>Register</strong> against the order and serial when you care about authenticity.</li>\n<li><strong>Policy</strong> with explicit windows, covered failure modes, and excluded SKUs or vendors.</li>\n<li><strong>Claim</strong> with the evidence that policy demands.</li>\n<li><strong>Resolve</strong> into Shopify: refund, replacement draft order, gift card, discount, store credit, or a custom out-of-band path that still records the decision.</li>\n</ol>\n<figure>\n<p>I shipped Claimify with warranty as a first-class path for exactly this reason. The App Store listing is under XCO Apps; the product thesis is the same either way: warranty is not a renamed form.</p>\n<p>Details and constraints from the build are on the <a href=\"https://ansezz.com/work/claimify-warranty-claims/\">Claimify work page</a>.</p>\n<h2>Resolutions that hit Shopify, not spreadsheets</h2>\n<p>A claim is not resolved when someone types \"approved\" in Slack. It is resolved when money or inventory moves in Shopify and the audit row says who did it, when, and why.</p>\n<p>That means resolution actions should map to real Admin API outcomes:</p>\n<ul>\n<li>Refund against the original order (partial or full).</li>\n<li>Replacement via draft order (or your fulfilment path) so inventory and shipping are first-class.</li>\n<li>Gift card, discount code, or store credit when that is the goodwill path.</li>\n<li>Custom resolution when the merchant still needs a recorded decision without an automatic money move.</li>\n</ul>\n<p>Copy-pasting order IDs between tabs is how refunds get double-issued and how replacements ship without a paper trail.</p>\n<p>Keep order context live from the GraphQL Admin API. Do not cache a stale snapshot as the source of truth for money.</p>\n<h2>Rules that take the easy half of the queue</h2>\n<p>Not every claim needs a human. Auto-approval and eligibility rules can handle the boring cases:</p>\n<ul>\n<li>Time since fulfilment (or since delivery) for returns vs warranty windows.</li>\n<li>Product tags, collections, vendors, and customer tags.</li>\n<li>Required photos for damage and some warranty types.</li>\n<li>Claim type and order totals that are safe to auto-approve under a cap.</li>\n</ul>\n<p>The point is not to remove judgment. It is to stop burning senior support time on \"yes, this is inside the 14-day window and the tag says returnable.\"</p>\n<p>Humans stay on the edge cases. Rules handle the rest, and every auto decision still writes an audit event.</p>\n<p>Public numbers from the Claimify listing, nothing invented: 5.0 from 6 reviews, plans at $9 / $19 / $49, and a 3-day trial. Useful as a product fact. Useless as a substitute for your own ops metrics.</p>\n\nPull last month's claims and mark the ones a simple rule (window, tag, photo present) could have approved. That share is your first auto-approval target.\n\n<h2>Engineering notes from shipping it</h2>\n<p>A few constraints that mattered when I built this as an embedded Shopify app (Laravel backend, Remix + App Bridge + Polaris admin, evidence on S3/R2):</p>\n<ul>\n<li><strong>Stay in Admin.</strong> Merchants abandon separate portals. App Bridge + Polaris keep triage where they already work.</li>\n<li><strong>Live order context.</strong> Pull line items and fulfilment from the GraphQL Admin API at decision time. Do not resolve against a stale copy.</li>\n<li><strong>Explicit state transitions.</strong> Every move is an event: for audit history, for webhooks, for Flow and helpdesk sync.</li>\n<li><strong>Evidence off the app servers.</strong> Signed uploads to object storage so photo blobs never choke the app process.</li>\n<li><strong>Protected customer data</strong> handled to Shopify's requirements, because claims are full of PII by nature.</li>\n</ul>\n<p>If you are scoping a similar public or private app, the same shape shows up on <a href=\"https://ansezz.com/services/shopify-plus-apps/\">Shopify Plus apps</a>: embedded admin, billing, webhooks, and a real resolution path into Shopify.</p>\n\n- A policy PDF is not a process. Inbox volume is not a claims system.\n- Returns, damage, and warranty can share one pipeline if claim type drives rules, evidence, and allowed resolutions.\n- Warranty needs registration, policy windows, and serials as first-class concepts, not a renamed return form.\n- Resolution means Shopify money or inventory moved, with an audit row, not a Slack emoji.\n- Auto-approval should clear the easy half of the queue; humans keep the edge cases.\n- Embed in Admin, keep order context live, treat state changes as events, store evidence outside the app process.\n\n<p>If your claims still live in a shared inbox, which claim type is creating the most silent backlog right now, and what would change if it had to move through one queue with an owner and a Shopify resolution?</p>\n</figure></figure>",
      "date_published": "2026-09-21T00:00:00.000Z",
      "date_modified": "2026-09-21T00:00:00.000Z",
      "tags": [
        "shopify",
        "shopify",
        "api-design",
        "production",
        "architecture"
      ],
      "image": "https://ansezz.com/blog/shopify-claims-pipeline-claimify/hero-a252260c.webp"
    },
    {
      "id": "https://ansezz.com/blog/mcp-first-is-the-new-mobile-first/",
      "url": "https://ansezz.com/blog/mcp-first-is-the-new-mobile-first/",
      "title": "MCP first is the new mobile first",
      "summary": "Mobile first made every screen work on a phone. MCP first makes every product capability work for an AI agent: tools, skills, and permissions before the human UI.",
      "content_html": "<p>Mobile first made products work on a phone. MCP first makes them work for an AI agent.</p>\n\n- The hard constraint used to be a small screen. Now it is whether an authorized agent can finish the job.\n- Ship tools, permissions and skills first. The human UI becomes one client of the same capabilities.\n- Start small: one sprint and three outcomes your users already ask for.\n\n<p>A decade ago the argument was settled in the hallway: <strong>mobile first</strong>. If the product did not work on a phone, it did not work.</p>\n<p>Teams stopped designing for a 27-inch monitor and shrinking later. They started with the constrained surface and let the desktop inherit the discipline.</p>\n<p>That constraint was a screen.</p>\n<p>Today the constraint is a <strong>capability contract</strong>. Users still want a UI, but more and more of the real work is asked in chat: Claude, Cursor, Grok Bot, an in-product agent.</p>\n<p>They do not want another settings page. They want to say what they need and have it done. Docs, support tickets, and click-paths become the fallback when the product cannot be <em>operated</em> by an agent.</p>\n<p>So the slogan flips: <strong>MCP first</strong>, or more precisely, <strong>agent first / skills first</strong>. Ship the tools, permissions, and judgment an agent needs <em>before</em> you polish the human interface that wraps them.</p>\n<p>I am not arguing that MCP replaces your API. I already wrote <a href=\"https://ansezz.com/blog/api-vs-mcp/\">API vs MCP</a> for that fork.</p>\n<p>MCP is how agents <em>discover and call</em> capabilities. Skills are how they apply judgment. The UI is how humans supervise. Get the order wrong and you bolt a chatbot onto a product that was never operable. Agents also need a map of what to read, and an <a href=\"https://ansezz.com/tools/llms-txt-generator/\">llms.txt file</a> at your site root is the simplest one to publish.</p>\n<h2>What \"MCP first\" actually means</h2>\n<p>MCP first does <strong>not</strong> mean \"add a Model Context Protocol server and call it a day.\"</p>\n<p>It means you design the product as a set of <strong>outcomes an agent can complete</strong>, with:</p>\n<ol>\n<li><strong>Named tools:</strong> create invoice, list delayed orders, refund within policy, rotate a key.</li>\n<li><strong>Clear inputs and outputs:</strong> schemas the model can fill without guessing.</li>\n<li><strong>Permissions:</strong> who the actor is, which tenant, which scopes (<a href=\"https://ansezz.com/blog/mcp-auth-audit-logging-laravel/\">auth and audit</a>).</li>\n<li><strong>Safe mutation rules:</strong> idempotency, rate limits, structured errors (<a href=\"https://ansezz.com/blog/mcp-idempotency-laravel-mutations/\">idempotency</a>, <a href=\"https://ansezz.com/blog/mcp-structured-tool-errors-laravel/\">tool errors</a>).</li>\n<li><strong>Skills:</strong> playbooks and guardrails so the agent knows <em>when</em> to call which tool, not just <em>that</em> a tool exists.</li>\n<li><strong>A human UI that consumes the same capabilities:</strong> screens become views and approvals over the same contract, not a parallel codebase.</li>\n</ol>\n<p>Mobile first forced responsive layout. MCP first forces <strong>operable capability</strong>.</p>\n<p>If an agent with the right credentials cannot complete the job end-to-end, the product is not finished, no matter how pretty the dashboard is.</p>\n<h2>The two-customer problem</h2>\n<figure>\n<p>Every serious product now has <strong>two customers</strong>:</p>\n<table>\n<thead>\n<tr>\n<th>Customer</th>\n<th>What they need</th>\n<th>Failure mode</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Human</td>\n<td>Screens, copy, empty states, accessibility</td>\n<td>Confusing UI; they open a ticket</td>\n</tr>\n<tr>\n<td>Agent</td>\n<td>Tools, schemas, errors they can recover from, audit trails</td>\n<td>Silent failure; retries; invented actions</td>\n</tr>\n</tbody>\n</table>\n<p>Most teams still ship for the human and hope the agent can scrape the DOM or call undocumented REST. That is the 2012 equivalent of \"we'll make the mobile site later.\"</p>\n<p>The fix is one capability layer. Humans get forms and tables. Agents get MCP (or an equivalent tool surface). Both hit the same domain services.</p>\n<p>When you add \"export CSV,\" you add the tool <em>and</em> the button, not only the button.</p>\n\nNext time you add a button, write the matching tool in the same pull request, calling the same domain service.\n\n<h2>Reverse the build sequence</h2>\n\n  <div>screens → API → maybe docs → maybe \"AI feature\"</div>\n  <div>outcomes → tools → permissions → skills → API/CLI → UI</div>\n\n<p>Concrete example: an ops tool for delayed Shopify orders.</p>\n<ol>\n<li><strong>Outcome:</strong> \"Find delayed orders for this shop and propose a next action within policy.\"</li>\n<li><strong>Tools:</strong> <code>list_delayed_orders</code>, <code>get_order</code>, <code>create_customer_message</code>, <code>issue_partial_refund</code> (scoped).</li>\n<li><strong>Permissions:</strong> shop actor, refund ceiling, PII redaction in logs.</li>\n<li><strong>Skills:</strong> when to refund vs reship vs escalate; never invent tracking numbers.</li>\n<li><strong>API:</strong> the same mutations your admin already uses.</li>\n<li><strong>UI:</strong> a queue page and an approval modal that call the <em>same</em> services.</li>\n</ol>\n<p>If you start with the queue page, the agent becomes a second-class citizen forever. If you start with the tools, the queue page is just one client.</p>\n<h2>Chat is a front door, not the product</h2>\n<figure>\n<p>People reach for chat because self-service still fails often enough to hurt. Industry surveys have long shown most customers try to solve a problem themselves before they contact a human, often cited around four in five attempts.</p>\n<p>Completion is the hard part. When the product cannot be operated by an agent with tools, chat becomes theater: a polite model that cannot refund, cannot look up the right tenant, and cannot tell you why the tool failed.</p>\n<p>Treat chat as a <strong>front door</strong>:</p>\n<ul>\n<li>Behind it: MCP tools (or skills that wrap them).</li>\n<li>Beside it: approval gates for irreversible money/PII moves.</li>\n<li>Under it: audit logs you would show a security reviewer.</li>\n<li>Past it: human support when the agent correctly escalates.</li>\n</ul>\n<p>No more \"read the docs, click twelve pages, then open a ticket.\" The docs become training data for skills. The pages become optional. Support becomes the exception path.</p>\n<h2>Skills first, not prompt first</h2>\n<p>A tool without a skill is a loaded gun with no safety briefing.</p>\n<p><strong>Skills</strong> (playbooks, runbooks, Cursor-style skill files, Claude Projects instructions; pick your stack) encode:</p>\n<ul>\n<li>which tools exist for this job</li>\n<li>sequencing and stop conditions</li>\n<li>what \"done\" looks like</li>\n<li>when to ask the human</li>\n</ul>\n<p>MCP first without skills first produces agents that call <code>refund_order</code> because the user sounded upset. Skills first without MCP first produces beautiful instructions with nothing to execute.</p>\n<p>Ship both. In practice I write the skill outline in the same PR as the first tool handlers: same review, same tenant tests.</p>\n<h2>What this is not</h2>\n<ul>\n<li><strong>Not</strong> \"delete the UI.\" Humans still approve, explore, and debug. Agent-first products often need <em>better</em> UIs for supervision.</li>\n<li><strong>Not</strong> \"every CRUD app needs a public MCP server tomorrow.\" Internal agent access (Cursor, Claude Desktop, your own bot) is enough to force the discipline. Public MCP is a distribution choice.</li>\n<li><strong>Not</strong> \"skip the API.\" MCP sits on capabilities. Thin, boring domain services still win.</li>\n<li><strong>Not</strong> \"let the model invent side effects.\" Auth, idempotency, structured errors, and rate limits are the mobile viewport of this era: constraints that make the product honest. That is why I shipped the <a href=\"https://ansezz.com/blog/mcp-auth-audit-logging-laravel/\">Laravel MCP safety trilogy</a> before preaching the slogan.</li>\n</ul>\n<h2>A one-sprint checklist</h2>\n<p>If you already have a product, you do not need a rewrite. In one sprint:</p>\n<ol>\n<li>Pick <strong>three</strong> high-value outcomes users already ask support for.</li>\n<li>Expose each as a tool with a schema, actor, and audit line.</li>\n<li>Return <strong>recoverable</strong> failures (<code>not_found</code>, <code>forbidden</code>, <code>validation_failed</code>) instead of empty 500s.</li>\n<li>Wrap mutations in idempotency keys.</li>\n<li>Write a short skill that sequences those tools and names the escalate path.</li>\n<li>Point your internal agent (Claude / Cursor / your bot) at it and dogfood for a week.</li>\n<li>Only then decide whether the human UI needs a new screen, or just a better approval affordance on top of what the agent already uses.</li>\n</ol>\n<p>If the agent cannot finish the job with a human in the loop for money moves, you learned something cheap. If it can, you just shipped mobile-first for the agent decade.</p>\n\nOpen your last month of support tickets and pick the three requests that come up most. Those are your first three tools.\n\n<h2>The hardest constraint first</h2>\n<p>Mobile first was never really about phones. It was about designing for the <strong>hardest real constraint</strong> first.</p>\n<p>The hard constraint now is: <em>can an authorized agent complete the work safely, with skills and tools, while a human supervises?</em></p>\n<p>Call it MCP first, agent first, or skills first; same idea. Prepare the tools and access. Then build the product. Chat becomes the place people ask; capability is what answers.</p>\n<p>If you are wiring this into multi-tenant Laravel, start with <a href=\"https://ansezz.com/blog/mcp-auth-audit-logging-laravel/\">auth and audit</a>, <a href=\"https://ansezz.com/blog/mcp-idempotency-laravel-mutations/\">idempotency</a>, and <a href=\"https://ansezz.com/blog/mcp-structured-tool-errors-laravel/\">structured tool errors</a>.</p>\n<p>If you are still choosing the connector shape, read <a href=\"https://ansezz.com/blog/api-vs-mcp/\">API vs MCP</a> first.</p>\n\n- Every product now has two customers: a human and an agent.\n- Build in this order: outcomes, tools, permissions, skills, API, then UI.\n- Chat is a front door. Tools, approval gates and audit logs do the work behind it.\n- Ship skills with tools, so the agent knows when to call what.\n\n<p>Which three things do your users ask support for that an agent with the right tools could finish today?</p>\n</figure></figure>",
      "date_published": "2026-09-20T00:00:00.000Z",
      "date_modified": "2026-09-20T00:00:00.000Z",
      "tags": [
        "architecture",
        "mcp",
        "architecture",
        "agentic-ai",
        "api-design",
        "ai",
        "claude"
      ],
      "image": "https://ansezz.com/blog/mcp-first-is-the-new-mobile-first/hero-1994379b.webp"
    },
    {
      "id": "https://ansezz.com/blog/mcp-structured-tool-errors-laravel/",
      "url": "https://ansezz.com/blog/mcp-structured-tool-errors-laravel/",
      "title": "MCP tool errors agents can actually recover from",
      "summary": "Throwing exceptions hides failures from the model. MCP wants isError tool results with actionable text, mapped from Laravel domain failures and audited like auth denials.",
      "content_html": "<p>When a tool fails, the model needs a sentence it can act on, not a crashed server.</p>\n\n- Business failures go back as a normal tool result with `isError: true`. JSON-RPC errors are for protocol problems only.\n- Catch once at the MCP adapter and map domain exceptions to a small, stable set of codes.\n- Each error needs a hint that names the next tool or field, and the same code goes into your audit log.\n\n<p><a href=\"https://ansezz.com/blog/mcp-auth-audit-logging-laravel/\">Auth and audit logging</a> keep a tool attributable. <a href=\"https://ansezz.com/blog/mcp-idempotency-laravel-mutations/\">Idempotency keys</a> keep a retry from charging twice. Neither helps if a failed tool call looks like a crashed server to the model.</p>\n<p>I keep seeing the same bug in Laravel MCP servers. A domain failure throws, the transport turns it into a JSON-RPC error (or an empty 500), and Claude never gets a sentence it can use to self-correct.</p>\n<p>The agent retries the same bad invoice ID, invents a different tool, or apologizes to the user. The invoice was simply not in that tenant.</p>\n<p>This post is the third gate I ship with every mutating MCP surface: <strong>tool execution errors that stay inside the tool result</strong>, carry a stable code, and tell the model what to try next, without leaking stack traces or other tenants' IDs.</p>\n<h2>The failure mode</h2>\n<p>A support agent asks Claude to fetch invoice <code>inv_9f2</code>. Your tool does this:</p>\n<pre><code>public function handle(string $invoiceId, McpActor $actor): array\n{\n    $invoice = Invoice::query()\n        -&gt;where('tenant_id', $actor-&gt;tenantId)\n        -&gt;where('id', $invoiceId)\n        -&gt;firstOrFail(); // ModelNotFoundException → 500 / JSON-RPC error\n\n    return $this-&gt;present($invoice);\n}\n</code></pre>\n<p><code>firstOrFail()</code> is correct for an HTTP controller that should return 404. Over MCP it is wrong.</p>\n<h3>Two channels for errors</h3>\n<p>The Model Context Protocol draws a hard line:</p>\n<ol>\n<li><strong>Protocol / request errors:</strong> unknown tool name, malformed arguments at the RPC layer. JSON-RPC <code>error</code> object. The client may surface these; models rarely recover well.</li>\n<li><strong>Tool execution errors:</strong> business failure, missing row, permission denial you already decided to explain, upstream timeout. Reported as a <strong>successful</strong> <code>tools/call</code> result with <code>isError: true</code> and actionable text in <code>content</code>.</li>\n</ol>\n<p>That second path is how the <a href=\"https://modelcontextprotocol.io/specification/2025-06-18/server/tools\">MCP tools specification</a> expects language models to self-correct. Hide the failure in a protocol error and you train the agent to flail.</p>\n<figure>\n<h2>Return isError. Do not throw across the wire.</h2>\n<p>Every tool handler ends in one of three shapes:</p>\n<pre><code>final class McpToolResult\n{\n    public static function ok(array $payload): array\n    {\n        $json = json_encode($payload, JSON_THROW_ON_ERROR);\n\n        return [\n            'content' =&gt; [['type' =&gt; 'text', 'text' =&gt; $json]],\n            'structuredContent' =&gt; $payload, // optional; keep in sync with outputSchema if you declare one\n            'isError' =&gt; false,\n        ];\n    }\n\n    public static function fail(\n        string $code,\n        string $message,\n        bool $retryable = false,\n        ?string $hint = null,\n    ): array {\n        $body = array_filter([\n            'error' =&gt; [\n                'code' =&gt; $code,\n                'message' =&gt; $message,\n                'retryable' =&gt; $retryable,\n                'hint' =&gt; $hint,\n            ],\n        ]);\n\n        $text = $hint\n            ? \"{$message} Hint: {$hint}\"\n            : $message;\n\n        return [\n            'content' =&gt; [['type' =&gt; 'text', 'text' =&gt; $text]],\n            // Prefer omitting structuredContent on errors unless your clients\n            // are known to skip outputSchema validation when isError is true.\n            'isError' =&gt; true,\n        ];\n    }\n}\n</code></pre>\n<p>Notes that bite in production:</p>\n<ul>\n<li><strong><code>isError: true</code> is still a successful RPC result.</strong> The call happened; the business outcome failed. That is the whole point.</li>\n<li><strong>Put the recovery text in <code>content</code>.</strong> Models read the text blocks. A code alone is not enough.</li>\n<li><strong>Be careful with <code>structuredContent</code> on errors</strong> if the tool declares <code>outputSchema</code>. Some clients still validate error payloads against the success schema. The safe default today: actionable text in <code>content</code>, no structured error object, unless you have verified your client stack.</li>\n<li><strong>Never put secrets, stack traces, SQL, or other tenants' identifiers in the text the model sees.</strong> Audit can keep a redacted internal reason; the agent gets a public message.</li>\n</ul>\n\nSearch your MCP tools for `firstOrFail()` and uncaught exceptions. Each one is a place where the model gets a crash instead of a hint.\n\n<h2>Map Laravel domain failures to a small code set</h2>\n<p>Do not invent a new error dialect per tool. Use one mapper, shared with the <a href=\"https://ansezz.com/blog/mcp-auth-audit-logging-laravel/\">auth gate</a> denial reasons and the <a href=\"https://ansezz.com/blog/mcp-idempotency-laravel-mutations/\">idempotency</a> conflict codes:</p>\n<table>\n<thead>\n<tr>\n<th>Code</th>\n<th>When</th>\n<th>Retry?</th>\n<th>Hint</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>not_found</code></td>\n<td>Missing in this tenant</td>\n<td>no</td>\n<td>List first; check the ID</td>\n</tr>\n<tr>\n<td><code>forbidden</code></td>\n<td>Actor lacks scope</td>\n<td>no</td>\n<td>Elevate or pick another resource</td>\n</tr>\n<tr>\n<td><code>validation_failed</code></td>\n<td>Args fail domain rules</td>\n<td>no</td>\n<td>Name the bad field and expected shape</td>\n</tr>\n<tr>\n<td><code>conflict</code></td>\n<td>Same key, different args</td>\n<td>no</td>\n<td>New key for a new intent</td>\n</tr>\n<tr>\n<td><code>in_flight</code></td>\n<td>Idempotency claim held</td>\n<td>yes</td>\n<td>Retry the <strong>same</strong> key shortly</td>\n</tr>\n<tr>\n<td><code>rate_limited</code></td>\n<td>Tool budget exhausted</td>\n<td>yes</td>\n<td>Back off; include Retry-After</td>\n</tr>\n<tr>\n<td><code>upstream_unavailable</code></td>\n<td>Provider blip</td>\n<td>yes</td>\n<td>Retry once; no second refund</td>\n</tr>\n<tr>\n<td><code>internal</code></td>\n<td>Truly unexpected</td>\n<td>no</td>\n<td>Stop; send the user to support</td>\n</tr>\n</tbody>\n</table>\n<pre><code>final class McpErrorMapper\n{\n    public function fromThrowable(\\Throwable $e, McpActor $actor): array\n    {\n        return match (true) {\n            $e instanceof ModelNotFoundException =&gt; McpToolResult::fail(\n                code: 'not_found',\n                message: 'Resource not found in your workspace.',\n                hint: 'List resources first, then retry with an ID from that list.',\n            ),\n            $e instanceof AuthorizationException =&gt; McpToolResult::fail(\n                code: 'forbidden',\n                message: 'You are not allowed to run this tool on that resource.',\n                hint: 'Ask the account owner for the required permission.',\n            ),\n            $e instanceof ValidationException =&gt; McpToolResult::fail(\n                code: 'validation_failed',\n                message: $e-&gt;validator-&gt;errors()-&gt;first() ?: 'Invalid arguments.',\n                hint: 'Fix the field named in the message and call again.',\n            ),\n            $e instanceof IdempotencyConflict =&gt; McpToolResult::fail(\n                code: 'conflict',\n                message: 'Idempotency key was reused with different arguments.',\n                hint: 'Generate a new idempotency_key for a new intent.',\n            ),\n            $e instanceof IdempotencyInFlight =&gt; McpToolResult::fail(\n                code: 'in_flight',\n                message: 'A request with this idempotency key is still running.',\n                retryable: true,\n                hint: 'Wait a few seconds and retry with the same idempotency_key.',\n            ),\n            default =&gt; McpToolResult::fail(\n                code: 'internal',\n                message: 'The tool failed unexpectedly. Do not retry with new side effects.',\n                hint: 'Tell the user support needs the session id '.$actor-&gt;sessionId.'.',\n            ),\n        };\n    }\n}\n</code></pre>\n<h3>Catch once, at the adapter</h3>\n<p>Catch at the MCP adapter boundary, once, not inside every tool with a different string. Tools throw domain exceptions. The adapter decides what the model is allowed to see.</p>\n<pre><code>public function invoke(string $tool, array $args, McpActor $actor): array\n{\n    try {\n        $this-&gt;auth-&gt;assertAllowed($actor, $tool, $args);\n        $result = $this-&gt;tools-&gt;run($tool, $args, $actor);\n        $this-&gt;audit-&gt;allowed($tool, $args, $result);\n\n        return McpToolResult::ok($result);\n    } catch (\\Throwable $e) {\n        $error = $this-&gt;errors-&gt;fromThrowable($e, $actor);\n        $this-&gt;audit-&gt;denied($tool, $args, $this-&gt;errors-&gt;codeOf($e));\n\n        return $error;\n    }\n}\n</code></pre>\n<p><code>not_found</code> must mean \"not in <strong>this</strong> tenant,\" the same rule as your global scopes from <a href=\"https://ansezz.com/blog/laravel-multi-tenancy/\">Laravel multi-tenancy</a>. A row that exists for another customer is still <code>not_found</code> to this actor.</p>\n<p>Never return <code>forbidden</code> with the other tenant's ID in the message. That is an enumeration leak dressed as honesty.</p>\n<h2>Hints beat apologies</h2>\n\n  <div>Something went wrong.</div>\n  <div>Invoice not found in your workspace. Hint: call `list_invoices` for this customer, then retry `get_invoice` with an ID from that list.</div>\n\n<p>The first one is useless. The model will guess.</p>\n<p>The hint should name <strong>another tool the agent already has</strong>, or a field to fix, not a URL to your internal runbook.</p>\n<p>You are writing for an LLM tool loop, not a human SRE.</p>\n<p>For <code>retryable: true</code>, say what to reuse. Idempotency taught us that \"retry\" without \"same key\" creates double refunds. Encode that in the hint every time:</p>\n<pre><code>A refund with this idempotency key is still in flight.\nHint: wait ~30s and call refund_order again with the SAME idempotency_key and arguments.\n</code></pre>\n\nRewrite your three most common error messages so each one ends with a hint that names a tool the agent already has.\n\n<h2>Audit the code the model saw</h2>\n<p>Your audit stream already records <code>allowed | denied | replayed</code>. Store the public error <code>code</code> on denials.</p>\n<p>When an agent loops on <code>not_found</code> fifty times, you want a query, not a vibes-based log scroll.</p>\n<figure>\n<pre><code>McpAuditEvent::query()-&gt;create([\n    'tenant_id' =&gt; $actor-&gt;tenantId,\n    'user_id' =&gt; $actor-&gt;userId,\n    'session_id' =&gt; $actor-&gt;sessionId,\n    'tool' =&gt; $tool,\n    'decision' =&gt; 'denied',\n    'reason' =&gt; $code, // not_found | forbidden | …\n    'args_hash' =&gt; hash('sha256', json_encode($redactedArgs)),\n    'args' =&gt; $redactedArgs,\n    'result_summary' =&gt; ['isError' =&gt; true, 'code' =&gt; $code],\n    'idempotency_key' =&gt; $args['idempotency_key'] ?? null,\n    'duration_ms' =&gt; $ms,\n]);\n</code></pre>\n<p>Ops dashboard: top denied codes per tool, per tenant, last 24h. Product signal: a spike in <code>validation_failed</code> usually means your tool schema description is lying to the model.</p>\n<h2>What I refuse to ship</h2>\n<ul>\n<li><code>firstOrFail()</code> / uncaught exceptions escaping the MCP adapter</li>\n<li>JSON-RPC errors for \"invoice missing\" or \"seat already reset\"</li>\n<li>Stack traces, SQLSTATE, or filesystem paths in <code>content</code> text</li>\n<li>Different error shapes per tool (<code>err</code> vs <code>error</code> vs <code>message</code> only)</li>\n<li><code>forbidden</code> messages that confirm another tenant's resource exists</li>\n<li>Swallowing idempotency <code>conflict</code> / <code>in_flight</code> into a generic <code>internal</code></li>\n<li>Marking money-moving failures <code>retryable: true</code> without a key</li>\n</ul>\n<p>If a human API would return 404/409/422 with a problem+json body, the agent path needs <code>isError</code> with a code and a hint, not a thrown Exception.</p>\n<h2>Wiring it into the series</h2>\n<p>This sits on top of <a href=\"https://ansezz.com/blog/mcp-auth-audit-logging-laravel/\">MCP auth and audit logging</a> and <a href=\"https://ansezz.com/blog/mcp-idempotency-laravel-mutations/\">idempotency keys for mutations</a>. Same <code>McpActor</code>, same audit table, same claim codes, now visible to the model instead of only to your pager.</p>\n<p>Tenancy rules stay the <a href=\"https://ansezz.com/blog/laravel-multi-tenancy/\">Laravel multi-tenancy</a> ones; do not invent a parallel \"agent ACL.\"</p>\n<p>For wallet-level token budgets at the LLM edge, that is a different layer. See <a href=\"https://ansezz.com/blog/rate-limiting-ai-wallet/\">rate limiting your AI wallet</a>. Tool errors are what happen <strong>after</strong> the model is already allowed to call you.</p>\n<p>If your MCP tools still throw through to the client and you want the error surface reviewed before agents hold live keys, <a href=\"https://ansezz.com/contact/?package=ai\">start with an AI Integration Sprint</a>. We will inventory which failures are recoverable, which must stop the loop, and how the audit trail proves it after a bad night.</p>\n\n- **Use MCP tool-result errors (`isError: true`) for business failures.** Reserve JSON-RPC errors for protocol problems.\n- **Catch once at the adapter.** Map domain exceptions to a small, stable code set.\n- **Write for the model:** message + hint that names the next tool or field; set `retryable` honestly.\n- **Omit or carefully handle `structuredContent` on errors** if `outputSchema` is in play.\n- **Audit the same codes** the agent saw. Spikes are product bugs, not just noise.\n- **Never leak cross-tenant existence** through clever error text.\n\n<p>When your agent hits a missing invoice today, does it get a hint it can act on, or a crash it can only apologize for?</p>\n</figure></figure>",
      "date_published": "2026-09-19T00:00:00.000Z",
      "date_modified": "2026-09-19T00:00:00.000Z",
      "tags": [
        "laravel",
        "laravel",
        "mcp",
        "agentic-ai",
        "security",
        "api-design",
        "multi-tenancy"
      ],
      "image": "https://ansezz.com/blog/mcp-structured-tool-errors-laravel/hero-622561a6.webp"
    },
    {
      "id": "https://ansezz.com/blog/mcp-auth-audit-logging-laravel/",
      "url": "https://ansezz.com/blog/mcp-auth-audit-logging-laravel/",
      "title": "MCP auth and audit logging in a Laravel SaaS",
      "summary": "An MCP server on Laravel is only as safe as its tenancy boundary. Scoped tool auth, request-bound actor context, and an audit log you can actually read at 3am.",
      "content_html": "<p>Giving Claude a tool that can read tickets and billing is useful. Giving it the same credentials your admin panel uses is how you invent a new incident class.</p>\n\n- Bind one actor (tenant, user, session, scopes) at the edge. Tools read it and never accept a tenant from the model.\n- Keep MCP tools thin and reuse the same finders and policies as your admin UI.\n- Log every attempt, allowed or denied, with redacted args, a reason and the idempotency key.\n\n<p>I have been wiring <a href=\"https://ansezz.com/blog/claude-mcp-dev-tools/\">MCP servers</a> into real Laravel SaaS surfaces, the same domain that powers the <a href=\"https://ansezz.com/work/mcp-saas-server/\">MCP SaaS case study</a>.</p>\n<p>The hard part is not JSON-RPC. The hard part is making sure every tool call is tenant-scoped, attributable, and auditable, without teaching the model a parallel permission system.</p>\n<p>This post is the pattern I keep shipping: actor context on the request, tools that refuse to invent a tenant, and an audit log that survives a prompt injection attempt.</p>\n<h2>The failure mode</h2>\n<p>Most first MCP demos look like this:</p>\n<ol>\n<li>Boot a Laravel app.</li>\n<li>Register <code>list_tickets</code>, <code>get_invoice</code>, <code>refund_order</code>.</li>\n<li>Hand the MCP client a service-account token that can do all three for every tenant.</li>\n<li>Call it a day.</li>\n</ol>\n<p>That works in a sandbox. In production it fails the moment:</p>\n<ul>\n<li>Two tenants share a deployment and the agent \"helpfully\" looks up the wrong invoice ID.</li>\n<li>A prompt-injected ticket description asks the agent to dump another customer's billing.</li>\n<li>A retry doubles a refund because nobody recorded the tool call idempotency key.</li>\n<li>You get paged and the logs say <code>tool=refund_order ok=true</code> with no actor, no tenant, and no arguments.</li>\n</ul>\n<p>MCP does not invent tenancy for you. Laravel already has it. The job is to force every tool through that gate.</p>\n<h2>One actor per request</h2>\n<figure>\n<p>Treat an MCP tool invocation like an HTTP request that happens to arrive over JSON-RPC. Bind an actor before any tool runs:</p>\n<pre><code>final class McpActor\n{\n    public function __construct(\n        public readonly string $tenantId,\n        public readonly string $userId,\n        public readonly string $sessionId,\n        /** @var list&lt;string&gt; */\n        public readonly array $scopes,\n    ) {}\n}\n</code></pre>\n<p>Resolve it once at the transport edge (API token, signed agent session, or OAuth subject) and stash it on the container for the lifetime of that call:</p>\n<pre><code>app()-&gt;instance(McpActor::class, $actor);\n</code></pre>\n<p>Tools never accept <code>tenant_id</code> from the model. If the model passes one, drop it. The only tenant that exists is the one on <code>McpActor</code>.</p>\n<p>That single rule kills the most common cross-tenant footgun: an LLM that \"found\" an ID in context and used it.</p>\n\nGrep your tool schemas for `tenant_id`. Remove it from every input and read it from `McpActor` instead.\n\n<h2>Tools are thin. Domain services stay thick.</h2>\n<p>Keep MCP handlers boring. Authorize, call the same service the Filament admin would call, return a shaped DTO:</p>\n<pre><code>final class GetInvoiceTool\n{\n    public function __construct(\n        private readonly McpActor $actor,\n        private readonly InvoiceFinder $invoices,\n        private readonly McpAuditLogger $audit,\n    ) {}\n\n    public function __invoke(array $args): array\n    {\n        $invoiceId = (string) ($args['invoice_id'] ?? '');\n\n        $invoice = $this-&gt;invoices-&gt;findForTenant(\n            tenantId: $this-&gt;actor-&gt;tenantId,\n            invoiceId: $invoiceId,\n        );\n\n        if ($invoice === null) {\n            $this-&gt;audit-&gt;denied('get_invoice', $args, 'not_found_or_foreign');\n            return ['error' =&gt; 'invoice_not_found'];\n        }\n\n        if (! in_array('billing:read', $this-&gt;actor-&gt;scopes, true)) {\n            $this-&gt;audit-&gt;denied('get_invoice', $args, 'missing_scope');\n            return ['error' =&gt; 'forbidden'];\n        }\n\n        $payload = $invoice-&gt;toMcpArray();\n        $this-&gt;audit-&gt;allowed('get_invoice', $args, $payload);\n\n        return $payload;\n    }\n}\n</code></pre>\n<p>Notice what is missing: a raw GraphQL passthrough, a <code>DB::table</code> query that forgets <code>tenant_id</code>, and any path where \"not found\" and \"wrong tenant\" diverge. Same response. Same audit reason code you can filter later.</p>\n<p>If you already enforce tenancy in Eloquent global scopes, keep them. MCP is not a reason to bypass them \"just this once.\"</p>\n<h2>Audit log shape that survives an incident</h2>\n<figure>\n<p>I log one row per tool attempt, allowed or denied, with enough to reconstruct the call without storing secrets:</p>\n<pre><code>McpAuditEvent::query()-&gt;create([\n    'tenant_id' =&gt; $actor-&gt;tenantId,\n    'user_id' =&gt; $actor-&gt;userId,\n    'session_id' =&gt; $actor-&gt;sessionId,\n    'tool' =&gt; $tool,\n    'decision' =&gt; $decision, // allowed | denied\n    'reason' =&gt; $reason,\n    'args_hash' =&gt; hash('sha256', json_encode($redactedArgs)),\n    'args' =&gt; $redactedArgs,\n    'result_summary' =&gt; $summary, // counts / IDs, never card data\n    'idempotency_key' =&gt; $idempotencyKey,\n    'duration_ms' =&gt; $ms,\n]);\n</code></pre>\n<p>Rules that have paid for themselves:</p>\n<ul>\n<li><strong>Redact before write.</strong> Tokens, raw card PANs, and full email bodies do not belong in <code>args</code>.</li>\n<li><strong>Hash + store.</strong> The hash lets you prove two calls carried the same payload without dumping PII into every analytics export.</li>\n<li><strong>Deny is a first-class event.</strong> Most teams only log successes. The interesting story is usually a denied <code>refund_order</code> right before a successful one from a different session.</li>\n<li><strong>Idempotency keys for mutations.</strong> Refunds, seat resets, and password emails must key on something the client retries with. Store it on the audit row and short-circuit duplicates.</li>\n</ul>\n<p>This is the same discipline I want on <a href=\"https://ansezz.com/blog/secure-agentic-commerce-shopify/\">agentic commerce checkouts</a>. Money-moving tools are just the obvious version of the rule.</p>\n\n  <div>`tool=refund_order ok=true`, with no actor, no tenant and no arguments.</div>\n  <div>Tenant, user, session, tool, decision, reason, redacted args, args hash, idempotency key and duration.</div>\n\n<h2>Scopes beat role names</h2>\n<p><code>is_admin</code> is a product concept. MCP wants capabilities:</p>\n<pre><code>tickets:read\ntickets:write\nbilling:read\nbilling:refund\ndocs:search\n</code></pre>\n<p>Map your SaaS roles onto scopes at session mint time. The tool checks scopes.</p>\n<p>When a customer upgrades from \"support viewer\" to \"billing ops,\" you change the session mint, not every tool.</p>\n<p>Also expire sessions aggressively. An MCP session that lives for a week is a stolen-laptop story waiting to happen.</p>\n\nWrite down the scopes each of your SaaS roles maps to, then check how long an MCP session lives before it expires.\n\n<h2>What I refuse to expose</h2>\n<p>A short deny-list that has saved me more than any middleware:</p>\n<ul>\n<li>Arbitrary Eloquent / Query Builder execution</li>\n<li>Raw GraphQL or \"run this SQL\" tools</li>\n<li>Cross-tenant search \"for convenience\"</li>\n<li>Tools that return full PII when a masked summary would do</li>\n<li>Mutations without an idempotency key</li>\n<li>Anything that bypasses the same policy class the HTTP API uses</li>\n</ul>\n<p>If a human support agent cannot do it in the admin UI, the model does not get a secret door.</p>\n<h2>Wiring it into the series</h2>\n<p>This sits next to <a href=\"https://ansezz.com/blog/laravel-multi-tenancy/\">Laravel multi-tenancy</a> and <a href=\"https://ansezz.com/blog/laravel-octane-high-traffic/\">Octane under load</a> on the framework side, and next to <a href=\"https://ansezz.com/blog/mcp-vs-a2a-vs-acp/\">MCP vs A2A</a> on the protocol side.</p>\n<p>The tenancy model is Laravel. The tool surface is MCP. The trust boundary is yours.</p>\n<p>If you are putting Claude on top of a multi-tenant Laravel app and want a second pair of eyes on the boundary, <a href=\"https://ansezz.com/contact/?package=ai\">start with an AI Integration Sprint</a>. We will map the tools, the scopes, and the audit trail before any agent sees production data.</p>\n\n- **Never let the model choose the tenant.** Bind `McpActor` at the edge; tools read it, they do not accept it.\n- **Thin tools, thick domain.** Reuse the same finders and policies as your admin UI.\n- **Audit allows and denies.** Include tool, actor, redacted args, reason, and idempotency key.\n- **Scopes over roles** at the MCP boundary. Mint short-lived sessions.\n- **No parallel API.** If it is dangerous in HTTP, it is dangerous in JSON-RPC.\n\n<p>If you got paged tonight about a strange refund, could your audit log tell you which actor, tenant and session asked for it?</p>\n</figure></figure>",
      "date_published": "2026-09-18T00:00:00.000Z",
      "date_modified": "2026-09-18T00:00:00.000Z",
      "tags": [
        "laravel",
        "laravel",
        "mcp",
        "agentic-ai",
        "security",
        "multi-tenancy"
      ],
      "image": "https://ansezz.com/blog/mcp-auth-audit-logging-laravel/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/mcp-idempotency-laravel-mutations/",
      "url": "https://ansezz.com/blog/mcp-idempotency-laravel-mutations/",
      "title": "Idempotency keys for MCP mutations in Laravel",
      "summary": "Refunds, seat resets, and password emails need more than audit logging. Client-supplied idempotency keys, server-side short-circuits, and race-safe storage under Octane.",
      "content_html": "<p>Agents retry. Transports retry. Humans mash \"send\" when the spinner hangs. Your mutation tools have to survive all three.</p>\n\n- Every MCP mutation needs a client-supplied idempotency key. Reads stay free.\n- The server short-circuits: replay the first result, reject a reused key with new args, and refuse duplicates still in flight.\n- A unique database index per tenant, tool and key is the real lock under Octane and Horizon.\n\n<p><a href=\"https://ansezz.com/blog/mcp-auth-audit-logging-laravel/\">Auth and audit logging</a> keep an MCP tool attributable. They do not keep a retry from charging the card twice.</p>\n<p>If your mutation tools treat every call as a new intent, you will invent double refunds, duplicate password emails, and seat resets that fire three times for one user request.</p>\n<p>This post is the follow-on pattern: client-supplied idempotency keys, a server short-circuit that returns the first result, keys stored on the same audit rows you already trust at 3am, and the race conditions that show up the moment you put Octane and Horizon in the path.</p>\n<h2>The failure mode is boring and expensive</h2>\n<p>A support agent asks Claude to refund order <code>ord_9f2</code>. The tool runs. The HTTP connection from the MCP client drops at 30s.</p>\n<p>The client retries with the same arguments. Your handler has no memory of the first call, so Stripe sees two refunds.</p>\n<p>Same story for:</p>\n<ul>\n<li><code>reset_seat</code>: second call wipes a freshly restored seat.</li>\n<li><code>send_password_reset</code>: user gets three emails in ninety seconds.</li>\n<li><code>apply_credit</code>: ledger entry lands twice because Horizon re-dispatched after a worker restart.</li>\n</ul>\n<p>None of these are exotic. They are the default behavior of any system that treats \"call arrived\" as \"intent is new.\"</p>\n<p>Models and clients will retry. Make the server safe when they do.</p>\n<p>The fix is not \"tell the model not to retry.\"</p>\n<h2>Client supplies the key. Server owns the semantics.</h2>\n<p>Every mutating tool requires an <code>idempotency_key</code> in the arguments. The client generates it (UUID v4 is fine) and reuses it for retries of the <em>same</em> intent.</p>\n<pre><code>final class RefundOrderTool\n{\n    public function __construct(\n        private readonly McpActor $actor,\n        private readonly IdempotencyGate $gate,\n        private readonly OrderRefunder $refunder,\n        private readonly McpAuditLogger $audit,\n    ) {}\n\n    public function __invoke(array $args): array\n    {\n        $key = trim((string) ($args['idempotency_key'] ?? ''));\n        if ($key === '' || strlen($key) &gt; 128) {\n            $this-&gt;audit-&gt;denied('refund_order', $args, 'missing_idempotency_key');\n            return ['error' =&gt; 'idempotency_key_required'];\n        }\n\n        if (! in_array('billing:refund', $this-&gt;actor-&gt;scopes, true)) {\n            $this-&gt;audit-&gt;denied('refund_order', $args, 'missing_scope');\n            return ['error' =&gt; 'forbidden'];\n        }\n\n        return $this-&gt;gate-&gt;run(\n            actor: $this-&gt;actor,\n            tool: 'refund_order',\n            key: $key,\n            args: $args,\n            execute: fn () =&gt; $this-&gt;refunder-&gt;refund(\n                tenantId: $this-&gt;actor-&gt;tenantId,\n                orderId: (string) $args['order_id'],\n                amountCents: (int) $args['amount_cents'],\n            ),\n        );\n    }\n}\n</code></pre>\n<p>Rules I enforce at the edge:</p>\n<ul>\n<li><strong>Required on mutations, ignored on reads.</strong> <code>get_invoice</code> does not need one. <code>refund_order</code> does not run without one.</li>\n<li><strong>Client-generated, not server-minted.</strong> If the server invents the key, a retry cannot present the same one.</li>\n<li><strong>Length-bounded.</strong> Cap at 128 chars. Reject empty, whitespace-only, and absurdly long strings.</li>\n<li><strong>Scoped to the actor's tenant.</strong> The uniqueness constraint is never global. See below.</li>\n</ul>\n<p>The model is allowed to <em>pass</em> the key. It is not allowed to invent tenancy, scopes, or \"just this once\" bypasses.</p>\n<p>Same discipline as the auth post: tools read <code>McpActor</code>, they do not accept <code>tenant_id</code> from the payload.</p>\n\nList every MCP tool that moves money, sends mail or resets state, and make `idempotency_key` a required argument on each one.\n\n<h2>Short-circuit before the domain runs</h2>\n<p>The gate is the whole product. First call executes and records. Later calls with the same key return the stored outcome without touching Stripe, the mailer, or the seat service.</p>\n<pre><code>final class IdempotencyGate\n{\n    public function __construct(\n        private readonly IdempotencyStore $store,\n        private readonly McpAuditLogger $audit,\n    ) {}\n\n    /**\n     * @param  callable(): array  $execute\n     * @return array&lt;string, mixed&gt;\n     */\n    public function run(\n        McpActor $actor,\n        string $tool,\n        string $key,\n        array $args,\n        callable $execute,\n    ): array {\n        $fingerprint = $this-&gt;fingerprint($args);\n        $claim = $this-&gt;store-&gt;claim(\n            tenantId: $actor-&gt;tenantId,\n            tool: $tool,\n            key: $key,\n            argsHash: $fingerprint,\n            ttlSeconds: 86_400,\n        );\n\n        if ($claim-&gt;status === ClaimStatus::Replay) {\n            $this-&gt;audit-&gt;replayed($tool, $args, $key, $claim-&gt;result);\n            return $claim-&gt;result;\n        }\n\n        if ($claim-&gt;status === ClaimStatus::Conflict) {\n            $this-&gt;audit-&gt;denied($tool, $args, 'idempotency_payload_mismatch');\n            return ['error' =&gt; 'idempotency_key_reuse_with_different_args'];\n        }\n\n        if ($claim-&gt;status === ClaimStatus::InFlight) {\n            $this-&gt;audit-&gt;denied($tool, $args, 'idempotency_in_flight');\n            return ['error' =&gt; 'request_in_flight'];\n        }\n\n        try {\n            $result = $execute();\n            $this-&gt;store-&gt;complete($actor-&gt;tenantId, $tool, $key, $result);\n            $this-&gt;audit-&gt;allowed($tool, $args, $result, $key);\n            return $result;\n        } catch (\\Throwable $e) {\n            $this-&gt;store-&gt;fail($actor-&gt;tenantId, $tool, $key, $e);\n            $this-&gt;audit-&gt;denied($tool, $args, 'execution_failed', $key);\n            throw $e;\n        }\n    }\n\n    private function fingerprint(array $args): string\n    {\n        $copy = $args;\n        unset($copy['idempotency_key']);\n        ksort($copy);\n\n        return hash('sha256', json_encode($copy, JSON_THROW_ON_ERROR));\n    }\n}\n</code></pre>\n<figure>\n<h3>Four outcomes</h3>\n<ol>\n<li><strong>Fresh claim:</strong> you own the key; run the mutation.</li>\n<li><strong>Replay:</strong> same key, same args hash; return the stored result.</li>\n<li><strong>Conflict:</strong> same key, <em>different</em> args hash; reject. Reusing a key for a different order is a bug or an attack, not a retry.</li>\n<li><strong>In flight:</strong> another worker holds the claim; tell the client to wait and retry with the <em>same</em> key. Do not start a second refund.</li>\n</ol>\n<p>That conflict case is non-negotiable. Idempotency is \"same intent, same result,\" not \"this UUID is a free pass for whatever args you send next.\"</p>\n<h2>Store the key on the audit row</h2>\n<p>You already write one audit event per tool attempt. Put the key there.</p>\n<p>When finance asks \"did we double-refund <code>ord_9f2</code>?\", you join on <code>idempotency_key</code> instead of guessing from timestamps.</p>\n<pre><code>McpAuditEvent::query()-&gt;create([\n    'tenant_id' =&gt; $actor-&gt;tenantId,\n    'user_id' =&gt; $actor-&gt;userId,\n    'session_id' =&gt; $actor-&gt;sessionId,\n    'tool' =&gt; $tool,\n    'decision' =&gt; $decision, // allowed | denied | replayed\n    'reason' =&gt; $reason,\n    'args_hash' =&gt; hash('sha256', json_encode($redactedArgs)),\n    'args' =&gt; $redactedArgs,\n    'result_summary' =&gt; $summary,\n    'idempotency_key' =&gt; $idempotencyKey,\n    'duration_ms' =&gt; $ms,\n]);\n</code></pre>\n<p><code>replayed</code> is a first-class decision. It is not a silent cache hit. Ops needs to see that the client retried and the server refused to mutate again.</p>\n<p>Index what you query:</p>\n<pre><code>Schema::table('mcp_audit_events', function (Blueprint $table) {\n    $table-&gt;string('idempotency_key', 128)-&gt;nullable();\n    $table-&gt;string('args_hash', 64)-&gt;nullable();\n    $table-&gt;index(['tenant_id', 'tool', 'idempotency_key']);\n});\n</code></pre>\n<h3>Two tables, two jobs</h3>\n<p>The audit table is the narrative. The idempotency store is the lock.</p>\n<p>Do not merge them into one table and hope. Audit wants append-only history; the gate wants a unique claim row you can update from <code>in_flight</code> to <code>completed</code>.</p>\n<h2>What to hash vs what to store</h2>\n<p>Separate three things people keep collapsing:</p>\n<table>\n<thead>\n<tr>\n<th>Field</th>\n<th>Purpose</th>\n<th>Store raw?</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>idempotency_key</code></td>\n<td>Client retry identity</td>\n<td>Yes, you need it for lookups and support</td>\n</tr>\n<tr>\n<td><code>args_hash</code></td>\n<td>Detect key reuse with different payloads</td>\n<td>Hash only of redacted, sorted args</td>\n</tr>\n<tr>\n<td><code>args</code> / <code>result_summary</code></td>\n<td>Incident reconstruction</td>\n<td>Redacted IDs and counts, never secrets</td>\n</tr>\n</tbody>\n</table>\n<p>Hash the fingerprint of the business args (order ID, amount, seat ID) after stripping the key itself. Store the raw key. Store redacted args on the audit row the same way you already do for reads.</p>\n<p>Do <strong>not</strong> hash the idempotency key into oblivion \"for privacy.\" You will need the original string when a client asks why their retry returned <code>request_in_flight</code>.</p>\n<p>You will also need it when you correlate MCP logs with Stripe's <code>Idempotency-Key</code> header if you forward the same value downstream.</p>\n<p>Forwarding is optional but clean: if Stripe already keys refunds, pass the MCP key through. One identity across your audit log and the payment provider.</p>\n<h2>Uniqueness is per tenant, per tool, with a TTL</h2>\n<p>A global unique index on <code>idempotency_key</code> alone is wrong in a multi-tenant SaaS. Two tenants can generate the same UUID. Two tools can legitimately reuse a key namespace if you are not careful.</p>\n<p>The claim identity is:</p>\n<pre><code>(tenant_id, tool, idempotency_key)\n</code></pre>\n<p>That matches how <a href=\"https://ansezz.com/blog/laravel-multi-tenancy/\">Laravel multi-tenancy</a> already scopes every other sensitive row. MCP is not special.</p>\n<h3>Keys are not forever</h3>\n<ul>\n<li><strong>24 hours</strong> is a sane default for refunds and seat mutations: long enough for flaky clients, short enough that the table does not become a second ledger.</li>\n<li><strong>Password reset emails</strong> can be shorter (1 to 2 hours). The harm window is smaller; the spam window is not.</li>\n<li>Expired keys may be reused for a <em>new</em> intent. Treat expiry like a new claim, not a silent replay of ancient results.</li>\n</ul>\n<pre><code>Schema::create('mcp_idempotency_claims', function (Blueprint $table) {\n    $table-&gt;id();\n    $table-&gt;string('tenant_id');\n    $table-&gt;string('tool', 64);\n    $table-&gt;string('idempotency_key', 128);\n    $table-&gt;string('args_hash', 64);\n    $table-&gt;string('status', 16); // in_flight | completed | failed\n    $table-&gt;json('result')-&gt;nullable();\n    $table-&gt;timestamp('expires_at');\n    $table-&gt;timestamps();\n\n    $table-&gt;unique(['tenant_id', 'tool', 'idempotency_key']);\n});\n</code></pre>\n<p>Prune expired rows on a schedule. Do not rely on \"we will never get collisions\" as a retention policy.</p>\n<h2>Races under Octane and Horizon</h2>\n<p>This is where the naive <code>SELECT</code> then <code>INSERT</code> dies.</p>\n<p><a href=\"https://ansezz.com/blog/laravel-octane-high-traffic/\">Octane</a> keeps workers warm. Two concurrent MCP calls for the same key can hit two workers in the same second.</p>\n<p>Horizon can re-run a job after a SIGKILL mid-refund. Cache locks help for the in-process case; they are not enough alone if the mutation is async.</p>\n<figure>\n<h3>What I ship</h3>\n<ol>\n<li><strong>Unique constraint in the database</strong> on <code>(tenant_id, tool, idempotency_key)</code>. The database is the source of truth for \"who won the claim.\"</li>\n<li><strong>Claim with <code>INSERT … ON CONFLICT</code> / <code>firstOrCreate</code> + catch unique violations.</strong> The loser reads the winner's row and follows Replay / InFlight / Conflict logic. Never \"just run it anyway.\"</li>\n<li><strong>Short Redis lock around the claim</strong> (a few seconds) to reduce thundering herds. This is an optional optimization, not a substitute for the unique index.</li>\n<li><strong>For Horizon-backed mutations</strong>, the job payload <em>includes</em> the idempotency key. The job itself goes through the same gate before calling Stripe. A worker restart must not create a second domain effect.</li>\n<li><strong>In-flight timeout.</strong> If a worker dies after claiming but before <code>complete</code>, the row sits in <code>in_flight</code>. Expire it (e.g. 60 to 120s) so a legitimate retry can reclaim, but only after you are sure the first side effect did not commit. Prefer making the downstream call itself idempotent (Stripe key, mail message-id) so reclaiming is safe.</li>\n</ol>\n<pre><code>public function claim(\n    string $tenantId,\n    string $tool,\n    string $key,\n    string $argsHash,\n    int $ttlSeconds,\n): ClaimResult {\n    try {\n        $row = McpIdempotencyClaim::query()-&gt;create([\n            'tenant_id' =&gt; $tenantId,\n            'tool' =&gt; $tool,\n            'idempotency_key' =&gt; $key,\n            'args_hash' =&gt; $argsHash,\n            'status' =&gt; 'in_flight',\n            'expires_at' =&gt; now()-&gt;addSeconds($ttlSeconds),\n        ]);\n\n        return ClaimResult::fresh($row);\n    } catch (UniqueConstraintViolationException) {\n        $existing = McpIdempotencyClaim::query()\n            -&gt;where('tenant_id', $tenantId)\n            -&gt;where('tool', $tool)\n            -&gt;where('idempotency_key', $key)\n            -&gt;firstOrFail();\n\n        return $this-&gt;interpretExisting($existing, $argsHash);\n    }\n}\n</code></pre>\n<p>If you only remember one sentence: <strong>the unique index is the lock; application locks are a courtesy.</strong></p>\n\nFire the same `refund_order` call twice in parallel against staging with one key. Exactly one refund should reach the payment provider.\n\n<h2>What I refuse to ship</h2>\n<ul>\n<li>Mutations that \"probably won't be retried\"</li>\n<li>Server-generated keys the client never sees</li>\n<li>Global idempotency uniqueness across tenants</li>\n<li>Silent success when the key matches but the args hash does not</li>\n<li>Storing card PANs or raw tokens in the claim <code>result</code> JSON</li>\n<li>Bypassing the gate inside a Horizon job \"because we already checked in the tool\"</li>\n<li>Infinite TTL that turns the claims table into an unbounded second database</li>\n</ul>\n<p>If a human admin UI would show a confirmation dialog before doing it twice, the agent path needs a key.</p>\n<h2>Wiring it into the series</h2>\n<p>This sits on top of <a href=\"https://ansezz.com/blog/mcp-auth-audit-logging-laravel/\">MCP auth and audit logging</a>: same <code>McpActor</code>, same audit stream, stricter rules for anything that moves money or mail.</p>\n<p>Tenancy remains the Laravel model from <a href=\"https://ansezz.com/blog/laravel-multi-tenancy/\">multi-tenancy</a>; concurrency reality is the <a href=\"https://ansezz.com/blog/laravel-octane-high-traffic/\">Octane</a> one.</p>\n<p>If you are exposing refund or billing tools over MCP and want the gate reviewed before an agent holds a live key, <a href=\"https://ansezz.com/contact/?package=ai\">start with an AI Integration Sprint</a>. We will map which tools mutate, how keys flow, and what the audit trail must prove after a retry storm.</p>\n\n- **Require client idempotency keys on every MCP mutation.** Reads stay free; refunds do not.\n- **Short-circuit on the server.** Replay the first result; conflict on arg mismatch; reject in-flight duplicates.\n- **Store the key on the audit row** and keep a separate claims table with `(tenant_id, tool, key)` uniqueness.\n- **Hash args for comparison; store keys raw; redact everything else.**\n- **TTL per tool class.** 24h for money, shorter for mail. Prune expired claims.\n- **Unique DB constraint beats Redis hope** under Octane workers and Horizon restarts.\n- **Downstream idempotency** (Stripe, mail providers) makes reclaim-after-crash safe.\n\n<p>If your MCP client dropped the connection mid-refund right now and retried, how many refunds would your customer see?</p>\n</figure></figure>",
      "date_published": "2026-09-18T00:00:00.000Z",
      "date_modified": "2026-09-18T00:00:00.000Z",
      "tags": [
        "laravel",
        "laravel",
        "mcp",
        "agentic-ai",
        "security",
        "multi-tenancy"
      ],
      "image": "https://ansezz.com/blog/mcp-idempotency-laravel-mutations/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/testing-ai-generated-code/",
      "url": "https://ansezz.com/blog/testing-ai-generated-code/",
      "title": "Trust is not a QA strategy: test AI code too",
      "summary": "AI code is 95% syntactically clean and fails security tests 45% of the time. Half of agentic PRs ship no tests. The verification pipeline I run instead.",
      "content_html": "<p>Trust is not a QA strategy.</p>\n\n- Clean formatting on AI code says nothing about whether it is secure or correct.\n- Half of agentic PRs change tested code without touching tests, and most agent-written tests assert almost nothing.\n- Gate on the diff: static analysis, diff coverage and mutation testing, with the spec written first.\n\n<p>It does not matter whether you are reviewing a pull request from a junior developer or accepting an entire feature an autonomous agent produced in ninety seconds. The engineering question is identical: does this code actually work, and how do you know?</p>\n<p>Somewhere in the shift to <a href=\"https://ansezz.com/blog/agentic-workflows-vibe-coding/\">agentic workflows</a>, a lot of teams quietly stopped asking the second half of that question.</p>\n<p>LLMs emit code that compiles, lints clean, and reads better than most of what humans write under deadline. So the code <em>looks</em> reviewed. Teams merge it, ship it, and find out what it does in production.</p>\n<h2>Syntax is not semantics</h2>\n<p>This is the part that catches experienced engineers off guard, because our instincts were trained on human output.</p>\n<p>Human code that looks sloppy usually <em>is</em> rough, and we review it accordingly. Human code that looks polished usually got there because someone iterated on it, which means someone thought about it. Polish was a proxy for scrutiny.</p>\n<p>LLMs break that correlation completely. Veracode's <a href=\"https://www.veracode.com/blog/spring-2026-genai-code-security/\">Spring 2026 GenAI Code Security update</a> measured a syntax correctness rate above 95% across the models it tested, against a security pass rate of 55%.</p>\n<p>The code is almost always well-formed. It is secure roughly half the time.</p>\n<p>Formatting stopped carrying any signal about correctness, and most review habits have not caught up.</p>\n<pre><code>// Generated in one shot. Clean names, correct types, zero lint warnings.\nfunction calculateDiscount(user: User, cartTotal: number): number {\n  if (user.isVip) {\n    return cartTotal * 0.2;\n  }\n  return cartTotal * 0.05;\n}\n</code></pre>\n<p>Nothing here is <em>wrong</em>, exactly. It is just unfinished in ways that only show up under real traffic.</p>\n<p>There is no guard against a negative <code>cartTotal</code> from a refund flow, no clamp on the maximum discount, no decision about what happens when <code>user</code> arrives undefined from a cache miss.</p>\n<p>The type signature says those cases cannot happen. The type signature is a claim about the code, not about production.</p>\n<h2>What the data actually says</h2>\n<p>Veracode's Spring 2026 report evaluated 80 coding tasks across Java, JavaScript, C#, and Python.</p>\n<p>The aggregate numbers are worth internalizing, because the failures are not evenly spread. They cluster hard around exactly the categories where a missing sanitizer is invisible in review.</p>\n<table>\n<thead>\n<tr>\n<th>CWE category</th>\n<th>Security pass rate</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Insecure cryptography (CWE-327)</td>\n<td>86%</td>\n</tr>\n<tr>\n<td>SQL injection (CWE-89)</td>\n<td>82%</td>\n</tr>\n<tr>\n<td>Cross-site scripting (CWE-80)</td>\n<td>15%</td>\n</tr>\n<tr>\n<td>Log injection (CWE-117)</td>\n<td>13%</td>\n</tr>\n</tbody>\n</table>\n<h3>Why encoding fails</h3>\n<p>Parameterized queries and modern crypto defaults are baked into the training data. Models get those right most of the time because the safe pattern <em>is</em> the common pattern.</p>\n<p>Output encoding is contextual. Whether a string needs HTML-escaping, attribute-escaping, or JS-escaping depends on where it lands in the template, and the model cannot see the template from inside the function it is writing. So it guesses, and it guesses wrong about six times out of seven.</p>\n<p>Language matters too. The same report puts Python at a 62% security pass rate and Java at 29%. If your stack is JVM-heavy, the baseline you are working from is worse than the headline number suggests.</p>\n<p>One genuinely encouraging data point: GPT-5 with extended reasoning hit 70 to 72%, the best pass rate Veracode has observed. That is real improvement. It is still a coin flip you would never accept from a human contributor.</p>\n\nIn your next review of agent-written code, check output encoding before anything else. It is the category that fails most.\n\n<h2>The testing gap in agentic pull requests</h2>\n<p>Chat completions are one thing. Autonomous agents opening PRs are another, and this is where the accounting gets uncomfortable.</p>\n<p>A July 2026 empirical study accepted at ICSME, <a href=\"https://arxiv.org/abs/2607.18057\">Test Coverage Analysis of Agentic Pull Requests</a>, analysed 4,882 agent-generated pull requests from the AIDev dataset: 532 Java, 4,350 Python, across five coding agents. Two findings should reshape how you gate merges.</p>\n<p><strong>Agents included test changes in only 49.6% of the PRs that touched code under test.</strong> Not greenfield repos with no test infrastructure: code that already sat inside a test suite, changed without the suite being touched.</p>\n<p><strong>Existing test suites did not pick up the slack.</strong> They executed 61.5% of the agents' changed executable lines in Java and just 27.0% in Python. In 64.8% of Python PRs, <em>not one changed line</em> was executed by any existing test. The safety net everyone assumes is there is mostly holes.</p>\n<p>Agents optimize for satisfying the prompt. Ask for a checkout feature and you get a controller, a migration, and a view, because that is what \"done\" looks like from the prompt's perspective.</p>\n<p>Boundary conditions, negative assertions, and concurrency cases are not in the prompt, so they are not in the diff.</p>\n<h2>Coverage numbers are lying to you</h2>\n<p>Here is the failure mode I see most often: a team wires up a coverage gate, sees 90%, and concludes the AI-authored code is verified.</p>\n<p>Line coverage measures execution, not verification. It tells you a line ran during the test suite. It says nothing about whether anything checked the result.</p>\n<p>A separate 2026 study, <a href=\"https://arxiv.org/abs/2606.18168\">All Smoke, No Alarm</a>, looked specifically at what agent-written tests assert. It covered 86,156 test-file patches from 33,596 agent-authored pull requests spanning 2,807 repositories (Codex, Copilot, Devin, Cursor, and Claude Code).</p>\n<p>80.2% of those patches contained weak or no explicit oracle signals. An agent rewarded for a green build learns very quickly that the cheapest path to green is an assertion that cannot fail.</p>\n<pre><code># 100% line coverage on process_payment. Verifies essentially nothing.\ndef test_process_payment():\n    result = process_payment(100)\n    assert result is not None\n</code></pre>\n<p>That test passes if <code>process_payment</code> charges the right amount. It also passes if it charges zero, double-charges, returns a stale object, or silently swallows a declined card. Coverage went up. Confidence should not have.</p>\n<h3>Mutation testing is the fix</h3>\n<p>The fix is mutation testing, which measures the thing you actually care about: if I break the code, does a test go red?</p>\n<p>Stryker for JS/TS, Infection for PHP, mutmut for Python, PIT for Java. Mutation score is the only coverage-adjacent metric I have found that agents cannot game by writing more assertions of nothing.</p>\n<h2>Error handling that hides the failure</h2>\n<p>Back in the ICSME coverage study, there is a number that gets less attention than it deserves. Existing tests miss 86.0% of the try/catch blocks agents touch in Java, and 81.0% of Python error-handling constructs. The recovery paths are the least-tested code in the diff.</p>\n<p>This is the worst possible failure shape. A crash is loud, has a stack trace, and pages someone.</p>\n<p>A <code>catch</code> block that logs a generic string and returns a default value produces a system that keeps running while quietly returning wrong answers. You do not find those in staging. You find them in a reconciliation report three weeks later.</p>\n<p>When you review agent output, read the <code>catch</code> blocks first. Ask what happens to the caller when the block executes, and whether the log line contains enough context to reconstruct the failure at 3am.</p>\n<p>That question is also the difference between <a href=\"https://ansezz.com/blog/logging-vs-monitoring/\">logging and monitoring</a>: a swallowed exception is logged, and monitored by nobody.</p>\n<h2>The throughput and stability trade</h2>\n<p>The <a href=\"https://dora.dev/dora-report-2025/\">2025 DORA State of AI-assisted Software Development</a> report is the macro version of everything above. Its central finding: higher AI adoption correlates with an increase in delivery throughput <em>and</em> an increase in delivery instability, at the same time.</p>\n<p>That is not an argument against AI. It is a statement about where the bottleneck moved.</p>\n<p>When change volume goes up and the control systems (automated testing, version control discipline, fast feedback) stay where they were, instability is the arithmetic result. DORA's own ROI model puts a number on it: in their sample calculation, a change failure rate drifting from 5% to 6% after AI adoption costs $344,000 in downtime.</p>\n<p>AI amplifies whatever your delivery process already was. That is the same trade-off I mapped out in <a href=\"https://ansezz.com/blog/ai-vs-traditional-development/\">AI vs traditional development</a>, now with a large-scale dataset behind it instead of an argument.</p>\n<h2>What I actually measure</h2>\n<p>Coverage percentage is a vanity metric here. These four are not:</p>\n<ul>\n<li><strong>Change failure rate, segmented.</strong> Tag agent-authored PRs at open time and compute CFR separately from human-authored changes. If you cannot split the two, you cannot tell whether AI is helping.</li>\n<li><strong>Mutation score on the diff.</strong> Not the repo: the changed lines. It answers \"would a test catch a regression here\" directly.</li>\n<li><strong>Test-change ratio.</strong> What share of code-modifying PRs also modify tests. Industry baseline is roughly 50%. Yours should be visible on a dashboard.</li>\n<li><strong>Diff coverage, not total coverage.</strong> Total coverage hides new code behind a large tested legacy base. Diff coverage exposes exactly the lines that just arrived.</li>\n</ul>\n\n  <div>Total line coverage at 90%, with weak assertions hiding behind a large tested legacy base.</div>\n  <div>Diff coverage, mutation score on the changed lines, and change failure rate split by who wrote the PR.</div>\n\n<h2>The verification pipeline</h2>\n<figure>\n<p>Manual review does not scale against agent output. A reviewer facing 800 lines of plausible-looking code per hour is not reviewing; they are skimming for style.</p>\n<p>The gate has to be automatic, and it has to run on the diff. I make the full case for <a href=\"https://ansezz.com/blog/stop-reading-code-ai-review/\">giving up line-by-line reading</a> separately; this is the tooling that has to exist first.</p>\n<pre><code>name: Agentic PR gate\non:\n  pull_request:\n    types: [opened, synchronize, reopened]\n\njobs:\n  sast:\n    runs-on: ubuntu-latest\n    container:\n      image: semgrep/semgrep\n    steps:\n      - uses: actions/checkout@v5\n      - run: semgrep ci --config=p/owasp-top-ten\n\n  diff-coverage:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v5\n        with:\n          fetch-depth: 0\n      - uses: shivammathur/setup-php@v2\n        with:\n          php-version: \"8.4\"\n          coverage: pcov\n      - run: composer install --no-interaction --prefer-dist\n      - run: vendor/bin/phpunit --coverage-cobertura=coverage.xml\n      - run: pipx install diff-cover\n      # Fail on new lines that no test executes — total coverage is irrelevant here.\n      - run: diff-cover coverage.xml --compare-branch=origin/${{ github.base_ref }} --fail-under=90\n\n  mutation:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v5\n        with:\n          fetch-depth: 0\n      - uses: shivammathur/setup-php@v2\n        with:\n          php-version: \"8.4\"\n          coverage: pcov\n      - run: composer install --no-interaction --prefer-dist\n      # Mutate only the changed files, and require tests to actually kill the mutants.\n      - run: |\n          vendor/bin/infection \\\n            --git-diff-filter=AM \\\n            --git-diff-base=origin/${{ github.base_ref }} \\\n            --min-msi=70 \\\n            --threads=max\n</code></pre>\n<p>Three layers, each catching what the previous one cannot:</p>\n<ul>\n<li>Semgrep's OWASP ruleset catches the XSS and injection patterns that dominate the failure table above.</li>\n<li><code>diff-cover</code> refuses the PR where new lines are unexecuted.</li>\n<li>Infection refuses the PR where new lines are executed but unasserted: the oracle problem, enforced by a machine rather than by a tired reviewer.</li>\n</ul>\n\nAdd the `diff-cover` job from the workflow above to one repo this week, and see how many recent agent PRs it would have blocked.\n\n<h3>Two practices upstream of the pipeline</h3>\n<p>These two are worth more than any of it:</p>\n<p><strong>Write the spec first.</strong> Give the agent the test file, the OpenAPI schema, or the behavioral contract before it writes the implementation. It is much better at satisfying an explicit oracle than at inventing one, and you keep authorship of the definition of correct.</p>\n<p><strong>Give the agent real context.</strong> A large share of the XSS failures come from the model not being able to see the rendering context. Wiring your agent into the actual schema, actual codebase, and actual docs (the <a href=\"https://ansezz.com/blog/claude-mcp-dev-tools/\">MCP tooling story</a>) measurably reduces guessing. It reduces it. It does not remove the need to verify.</p>\n<figure>\n<p>None of this is an argument for writing code by hand. I ship agent-authored code every week.</p>\n<p>The argument is that verification is the part of the job that did not get automated, and pretending otherwise just moves the discovery of your bugs from CI to your customers.</p>\n<p>If the only thing between an agent-authored PR and production today is \"a human skimming the diff,\" that is worth fixing this quarter. <a href=\"https://ansezz.com/contact/\">Tell me what your stack looks like</a> and I will tell you where I would put the first gate. 🤘</p>\n\n- **Polish stopped being a proxy for scrutiny.** 95% syntax correctness against a 55% security pass rate means clean formatting now tells you nothing about whether the logic holds.\n- **Failures cluster in context-dependent categories.** Crypto and SQL injection pass 82 to 86% of the time. XSS and log injection pass 13 to 15%. Review output encoding first.\n- **Half of agentic PRs touch code without touching tests**, and existing suites execute as little as 27% of the changed lines. Gate on test presence, not good intentions.\n- **Mutation score over line coverage.** 80.2% of agent-written test patches carry weak or absent oracles. Only mutation testing catches an assertion that cannot fail.\n- **Read the catch blocks first.** Test suites miss over 80% of the error-handling paths agents touch, which means the likely production symptom is a wrong answer, not a crash.\n\n<p>What is currently standing between an agent-authored PR and production in your pipeline?</p>\n</figure></figure>",
      "date_published": "2026-08-09T00:00:00.000Z",
      "date_modified": "2026-08-09T00:00:00.000Z",
      "tags": [
        "ai",
        "ai",
        "code-quality",
        "agentic-ai",
        "ci-cd"
      ],
      "image": "https://ansezz.com/blog/testing-ai-generated-code/hero-96850ff4.webp"
    },
    {
      "id": "https://ansezz.com/blog/secure-agentic-commerce-shopify/",
      "url": "https://ansezz.com/blog/secure-agentic-commerce-shopify/",
      "title": "Building secure agentic commerce on Shopify in 2026",
      "summary": "How agentic commerce on Shopify stays safe: UCP negotiation, an MCP governance layer, scoped credentials, signed requests, and human approval at checkout.",
      "content_html": "<p>An AI agent can discover a product, build a cart, and reach checkout faster than a human. It can also make an expensive mistake in seconds.</p>\n\n- Never hand an agent broad Shopify credentials. Put an MCP governance server between the agent and the store.\n- UCP profile negotiation, signed requests and idempotency keys are security controls, not formalities.\n- Human approval is a checkout state. Respect `requires_escalation` instead of routing around it.\n\n<p>Agentic commerce changes the security boundary of a Shopify store. A storefront is no longer only serving pages to people.</p>\n<p>It exposes product data, pricing, inventory, fulfillment options, checkout actions, and order state to autonomous software.</p>\n<p>That creates a real problem. If an agent receives broad Shopify credentials, a prompt injection or an implementation bug can expose customer data or trigger an unauthorized transaction.</p>\n<p>The fix isn't to block agents. It's to give them a narrow, observable, policy-controlled path through the commerce stack. The principle is short:</p>\n<p>Let the agent reason about commerce, but never let it define its own permissions.</p>\n<h2>The security problem in agentic commerce</h2>\n<p>Traditional Shopify integrations assume a known application, a fixed workflow, and a human reviewing the final checkout screen. Autonomous agents remove all three assumptions.</p>\n<p>A buyer agent may:</p>\n<ol>\n<li>Interpret a natural-language request.</li>\n<li>Search product catalogs.</li>\n<li>Compare variants, prices, and delivery options.</li>\n<li>Build a cart across multiple turns.</li>\n<li>Select a payment handler.</li>\n<li>Complete checkout or hand the buyer to a merchant page.</li>\n<li>Monitor fulfillment, refunds, returns, or cancellations.</li>\n</ol>\n<p>Every step carries model uncertainty. The agent may misread intent. Product data may be stale. A tool description may be too broad. An external page may carry malicious instructions. A retry may duplicate an action that moves money.</p>\n<p>This is why agentic commerce needs a different engineering model. The agent should never hold unrestricted access to Shopify Admin APIs. It should reach approved commerce capabilities through a controlled protocol layer.</p>\n<h2>How Shopify agentic commerce works with UCP</h2>\n<p>The <strong>Universal Commerce Protocol (UCP)</strong> gives agents and merchants a standard way to describe capabilities, negotiate compatibility, and execute commerce workflows.</p>\n<p>Shopify's UCP implementation covers the main buyer journey:</p>\n<ul>\n<li>Negotiation and authentication</li>\n<li>Product discovery</li>\n<li>Cart creation and updates</li>\n<li>Checkout creation and completion</li>\n<li>Order monitoring and post-purchase events</li>\n</ul>\n<p>UCP is not a replacement for Shopify's commerce APIs. It's a standardized interaction layer so different agents and merchant systems can speak in shared commerce concepts.</p>\n<p>It supports multiple transports (REST, JSON-RPC, MCP, and A2A, plus the JSON-RPC 2.0 channel behind Shopify's Embedded Checkout Protocol) while the business logic stays the same.</p>\n<h3>Profile negotiation</h3>\n<p>The mechanism that matters for security is profile negotiation. A merchant publishes a business profile, usually at:</p>\n<pre><code>https://store.example.com/.well-known/ucp\n</code></pre>\n<p>On Shopify, that profile is tied to the merchant storefront. It's a self-contained document declaring <code>version</code>, <code>services</code>, <code>capabilities</code>, <code>payment_handlers</code>, and (via <code>supported_versions</code>) the profile URIs for older protocol versions. (See the <a href=\"https://ansezz.com/blog/shopify-ucp-quick-start/\">Shopify UCP quick-start</a> for getting the manifest live.)</p>\n<p>The agent hosts its own platform profile at an HTTPS URL and passes that URL on every request through the <code>UCP-Agent</code> header. The platform profile mirrors the business one and adds a <code>signing_keys</code> registry.</p>\n<p>That single document is both the capability declaration and the key-resolution mechanism, which is why profile validation is a security control, not a formality.</p>\n<p>Shopify then computes the intersection:</p>\n<pre><code>merchant capabilities ∩ agent capabilities = negotiated capabilities\n</code></pre>\n<p>Only compatible capabilities become active. Extensions (<code>dev.ucp.shopping.discounts</code>, <code>dev.ucp.shopping.buyer_consent</code>, <code>dev.ucp.shopping.fulfillment</code>) carry an <code>extends</code> field naming their parent capability, and they never auto-activate. If the parent isn't in the negotiated set, the extension drops out with it.</p>\n<figure>\n<p>A trimmed platform profile, showing the shape rather than a complete document:</p>\n<pre><code>{\n  \"version\": \"2026-04-08\",\n  \"services\": {\n    \"dev.ucp.shopping\": {}\n  },\n  \"capabilities\": {\n    \"dev.ucp.shopping.cart\": {},\n    \"dev.ucp.shopping.checkout\": {}\n  },\n  \"signing_keys\": [\n    {\n      \"kty\": \"EC\",\n      \"crv\": \"P-256\",\n      \"kid\": \"agent-2026-08\",\n      \"x\": \"...\",\n      \"y\": \"...\"\n    }\n  ]\n}\n</code></pre>\n<p>Protocol versions are still moving through 2026. Check the <a href=\"https://ucp.dev/specification/reference/\">UCP specification</a> for the current registry shape.</p>\n<p>Pin the version your integration uses, validate it strictly, and test version negotiation before rollout. Never silently downgrade to an older capability set.</p>\n<p>For the wider shift from visual storefronts to programmable commerce, see <a href=\"https://ansezz.com/blog/agentic-commerce-shopify/\">why agentic commerce will change the way you build Shopify stores</a>.</p>\n<h2>How do you build secure agentic commerce?</h2>\n<p>You need an intermediate control plane between the agent and Shopify:</p>\n<pre><code>buyer\n  ↓\nAI agent\n  ↓\nMCP client\n  ↓\nintermediate MCP governance server\n  ↓\nUCP commerce tools\n  ↓\nShopify storefront and checkout services\n</code></pre>\n<p>That governance server is the whole point. It keeps the model off sensitive APIs and gives the team one place to enforce policy.</p>\n<h3>1. Expose task-specific tools</h3>\n<p>Don't expose a generic <code>shopify_admin_request</code>. Create narrow tools with explicit inputs and outputs:</p>\n<pre><code>search_products(query, filters)\nget_variant(variant_id)\ncreate_cart(lines, buyer_country)\nupdate_cart(cart_id, lines)\ncreate_checkout(cart_id)\nget_order(order_id)\n</code></pre>\n<p>One tool, one well-defined job, every parameter validated against a schema before any downstream request.</p>\n<p>Never accept arbitrary GraphQL from an agent. Use fixed queries and allowlisted fields. That prevents data overexposure and shrinks the blast radius of prompt injection.</p>\n\n  <div>One generic `shopify_admin_request` tool that accepts arbitrary GraphQL.</div>\n  <div>Narrow tools like `search_products` and `create_checkout`, fixed queries and allowlisted fields.</div>\n\n<h3>2. Keep Admin tokens server-side</h3>\n<p>Admin API tokens belong in a secret manager. Never in:</p>\n<ul>\n<li>Agent prompts</li>\n<li>Tool responses</li>\n<li>Browser JavaScript</li>\n<li>Client-side logs</li>\n<li>Model context</li>\n<li><code>agents.md</code></li>\n<li>Product descriptions or metafields</li>\n</ul>\n<p>The governance server holds the credential and calls only the minimum Shopify operations required. For public product discovery, prefer Storefront API access with appropriate scopes. Reserve Admin API access for back-office operations that genuinely need it.</p>\n<p>Use separate credentials for:</p>\n<ul>\n<li>Catalog reads</li>\n<li>Cart and checkout operations</li>\n<li>Order status reads</li>\n<li>Fulfillment or refund workflows</li>\n<li>Internal administrative actions</li>\n</ul>\n<p>Revocation and incident analysis get much easier when the blast radius is already partitioned.</p>\n\nSearch your prompts, tool responses, logs and `agents.md` for any Admin API token. Move every one you find into a secret manager.\n\n<h3>3. Apply capability and state policies</h3>\n<p>Authorization depends on both the tool and the current commerce state.</p>\n<table>\n<thead>\n<tr>\n<th>Commerce state</th>\n<th>Allowed actions</th>\n<th>Approval requirement</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Discovery</td>\n<td>Search products, read public availability</td>\n<td>None</td>\n</tr>\n<tr>\n<td>Cart building</td>\n<td>Add or remove line items, estimate totals</td>\n<td>Optional</td>\n</tr>\n<tr>\n<td>Pre-checkout</td>\n<td>Select fulfillment, validate buyer data</td>\n<td>Buyer confirmation</td>\n</tr>\n<tr>\n<td>Checkout completion</td>\n<td>Submit payment mandate or complete checkout</td>\n<td>Strong authentication</td>\n</tr>\n<tr>\n<td>Post-purchase</td>\n<td>Read order status, track fulfillment</td>\n<td>Scoped buyer access</td>\n</tr>\n</tbody>\n</table>\n<p>A signed agent is not automatically allowed to complete a purchase. High-risk actions need the right authorization tier, scope, buyer identity, and payment consent.</p>\n<p>Treat every checkout completion as a state transition, not an ordinary tool call.</p>\n<h2>Secure the protocol and the request envelope</h2>\n<p>The protocol has to authenticate more than the model. It authenticates the agent profile, request origin, capability set, and transaction context.</p>\n<p>UCP signs every HTTP-based transport with <a href=\"https://www.rfc-editor.org/rfc/rfc9421.html\">RFC 9421 HTTP Message Signatures</a>. Signing is asymmetric: ES256 is mandatory, ES384 optional.</p>\n<p>Public keys are JWKs (RFC 7517) published in the <code>signing_keys</code> array of the signer's profile, and the body is bound in through a <code>Content-Digest</code> header (RFC 9530) computed over the raw body bytes.</p>\n<h3>What the signature covers</h3>\n<p>A request signature covers <code>@method</code>, <code>@authority</code>, <code>@path</code>, the query, <code>ucp-agent</code>, <code>idempotency-key</code>, and the content headers. Responses cover <code>@status</code> instead of the method.</p>\n<p>That component list is the security property worth internalizing. The idempotency key is <em>inside</em> the signature, so a replayed request can't be re-pointed at a new operation without invalidating it.</p>\n<p>UCP deliberately handles replay protection at the business layer through idempotency keys rather than at the signature layer; the RFC 9421 <code>created</code> parameter is optional. If your governance server treats idempotency as an optional convenience, you've removed the replay control.</p>\n<p>At the governance layer, verify:</p>\n<ul>\n<li>The profile URL from <code>UCP-Agent</code> uses HTTPS.</li>\n<li>The profile is valid JSON and within size limits.</li>\n<li>The declared UCP version is supported.</li>\n<li>Capabilities use compatible versions.</li>\n<li>Extension <code>extends</code> dependencies are satisfied.</li>\n<li>The signing key resolves from the declared profile's <code>signing_keys</code>.</li>\n<li>The signature base reconstructs from the required components.</li>\n<li>The <code>Content-Digest</code> matches the raw body you actually read.</li>\n<li>Idempotency keys are enforced, stored, and honored on retry.</li>\n</ul>\n<h3>HMAC for your own endpoints</h3>\n<p>For custom endpoints, webhooks, or internal proxy routes, HMAC adds a shared-secret check. HMAC does not replace UCP's asymmetric message-signature model. It's an extra control where both parties already share a secret.</p>\n<p>A webhook verifier should:</p>\n<ol>\n<li>Read the raw request body.</li>\n<li>Compute the HMAC using the expected secret.</li>\n<li>Compare signatures with a constant-time function.</li>\n<li>Reject stale timestamps.</li>\n<li>Record the event ID to prevent replay.</li>\n<li>Process the event idempotently.</li>\n</ol>\n<p>In Laravel terms:</p>\n<pre><code>$expected = hash_hmac(\n    'sha256',\n    $timestamp . '.' . $rawBody,\n    $sharedSecret\n);\n\nif (!hash_equals($expected, $receivedSignature)) {\n    abort(401, 'Invalid signature');\n}\n</code></pre>\n<p>Never verify an HMAC over a parsed and re-serialized JSON object. Whitespace and key ordering change the bytes. Verify the original payload.</p>\n<figure>\n<h2>Human approval is a protocol state</h2>\n<p>Autonomous doesn't mean every action is automatic.</p>\n<p>UCP models graceful escalation through checkout states such as:</p>\n<ul>\n<li><code>incomplete</code>: required information is missing</li>\n<li><code>requires_escalation</code>: buyer input is needed</li>\n<li><code>ready_for_complete</code>: everything is collected and the agent may finalize programmatically</li>\n</ul>\n<p>Those three sit alongside <code>complete_in_progress</code>, <code>completed</code>, and <code>canceled</code> in the checkout state machine. A checkout can move back and forth between <code>incomplete</code> and <code>requires_escalation</code>, and it can reach <code>canceled</code> from anywhere.</p>\n<p>An agent may select a product and calculate shipping but still need buyer input for a 3DS challenge, address validation, terms acceptance, age verification, or custom selling terms.</p>\n<h3>Escalation is not an error</h3>\n<p>A <code>requires_escalation</code> response must carry a <code>continue_url</code> and at least one message at escalation severity. That's the merchant handing you a place to send the buyer, plus the reason. Send them there with the cart intact instead of starting over.</p>\n<p>Your agent should not try to route around this state. It should surface the escalation messages verbatim and preserve the exact checkout context the merchant returned.</p>\n<p>The same applies to compliance disclosures. A <code>warning</code> message with <code>presentation: \"disclosure\"</code> (Prop 65, allergens, age restrictions) must be displayed next to the item it applies to, never collapsed or auto-dismissed. If your surface can't render it faithfully, escalate to <code>continue_url</code> rather than silently downgrading.</p>\n<p>Add approval gates for:</p>\n<ul>\n<li>Checkout completion</li>\n<li>Orders above a configured threshold</li>\n<li>Address changes</li>\n<li>Discounts outside policy</li>\n<li>Refunds and cancellations</li>\n<li>Regulated or age-restricted products</li>\n<li>Unusual quantity or velocity patterns</li>\n</ul>\n<p>Use idempotency keys on every money-moving request. A retry must not create a second checkout or a duplicate order.</p>\n<h2>Use <a href=\"http://agents.md\">agents.md</a> for discovery and operational policy</h2>\n<p>An <code>agents.md</code> file helps compatible tools discover how your application expects agents to behave. Treat it as documentation and policy guidance, never as proof of identity or authorization.</p>\n<p>A useful file describes safe defaults, tool boundaries, and failure handling:</p>\n<pre><code># Commerce agent guidance\n\n## Safe defaults\n\n- Use catalog tools for product discovery.\n- Never request or expose Shopify Admin tokens.\n- Ask for buyer confirmation before checkout completion.\n- Respect `requires_escalation` responses.\n\n## Tool boundaries\n\n- Do not call arbitrary GraphQL.\n- Do not infer inventory when the API returns unknown.\n- Do not modify orders without an authenticated buyer session.\n\n## Failure handling\n\n- Retry read operations with backoff.\n- Use idempotency keys for writes.\n- Log tool name, request ID, policy decision, and result status.\n</code></pre>\n<p>Keep it version-controlled and review changes like code. Sign or hash the policy bundle if your architecture supports it. A compromised instruction file must never grant permissions the server itself doesn't enforce.</p>\n<p>For more on MCP tool boundaries and context control, see <a href=\"https://ansezz.com/blog/claude-mcp-dev-tools/\">Claude MCP and real-time developer tools</a>.</p>\n<p>The same least-privilege rules apply to commerce agents, and the <a href=\"https://ansezz.com/blog/agentic-workflows-vibe-coding/\">agentic workflow architecture guide</a> covers human approval and stateful tool loops.</p>\n<h2>Launch checklist</h2>\n<p>Secure agentic commerce is an API and policy engineering problem, not a prompt design problem. Before launch:</p>\n<ul>\n<li>Publish and validate UCP profiles over HTTPS.</li>\n<li>Pin supported protocol and capability versions.</li>\n<li>Put an intermediate MCP governance server between agents and Shopify.</li>\n<li>Expose narrow, schema-validated tools instead of arbitrary API access.</li>\n<li>Keep Admin tokens in a server-side secret manager.</li>\n<li>Use scoped credentials for catalog, checkout, orders, and administration.</li>\n<li>Verify HTTP Message Signatures and rotate keys.</li>\n<li>Add HMAC verification to custom webhooks and proxy endpoints.</li>\n<li>Require idempotency for all writes and payment-related operations.</li>\n<li>Model checkout as explicit states with human escalation.</li>\n<li>Treat <code>agents.md</code> as guidance, never as authorization.</li>\n<li>Log every tool call, policy decision, signature result, and transaction identifier.</li>\n<li>Test malformed profiles, replay attempts, prompt injection, stale inventory, duplicate retries, and unauthorized checkout attempts.</li>\n</ul>\n<p>The opportunity in agentic commerce is large, but trust decides which implementations survive production. If you want a second pair of eyes on that boundary, <a href=\"https://ansezz.com/services/\">here's how I work with teams</a>.</p>\n\n- Keep the agent on narrow, schema-validated tools behind a governance server. Admin tokens stay server-side.\n- Validate UCP profiles, pin versions, and verify signatures and content digests on every request.\n- Idempotency keys are your replay control. Treat them as required.\n- Model checkout as states, and send the buyer to `continue_url` when escalation is needed.\n\n<p>What authorization boundary stops your Shopify agent from turning a reasonable buyer request into an unauthorized transaction?</p>\n</figure></figure>",
      "date_published": "2026-08-07T00:00:00.000Z",
      "date_modified": "2026-08-07T00:00:00.000Z",
      "tags": [
        "shopify",
        "shopify",
        "agentic-commerce",
        "mcp",
        "security",
        "ai",
        "architecture"
      ],
      "image": "https://ansezz.com/blog/secure-agentic-commerce-shopify/hero-bfdc7525.webp"
    },
    {
      "id": "https://ansezz.com/blog/mcp-vs-a2a-vs-acp/",
      "url": "https://ansezz.com/blog/mcp-vs-a2a-vs-acp/",
      "title": "MCP vs A2A vs ACP: the agent protocol stack",
      "summary": "MCP connects agents to tools, A2A connects agents to each other, and ACP has folded into A2A. How the two-layer agent protocol stack fits together in 2026.",
      "content_html": "<p>The missing piece in most multi-agent systems is not model quality. It is a communication standard.</p>\n\n- MCP connects agents to tools and data. A2A connects agents to each other.\n- ACP, the third name you will see in older posts, has merged into A2A.\n- Most agents play both roles at once: they use tools over MCP and answer peers over A2A.\n\n<p>You are ready to move past single-prompt chatbots and build a real multi-agent system. Then you look at what you actually have: agents that cannot reach your Postgres database without a bespoke script, and cannot coordinate with the agent another team shipped last quarter without a shared Slack channel and a human in the middle.</p>\n<p>Without a shared standard, you spend most of your engineering time writing glue code for integrations and very little on the behavior that makes the agents worth running. Every schema change breaks a hand-rolled adapter somewhere.</p>\n<p>The good news is that the landscape has converged. Two protocols now cover the two axes that matter: the Model Context Protocol (MCP) and Agent2Agent (A2A). The Agent Communication Protocol (ACP) has been folded into A2A.</p>\n<p>This post breaks down what each layer does and how to wire them together.</p>\n<h2>The current state of agent protocols</h2>\n<p>In early 2025 you had to pick a vendor SDK and live with it. Function calling looked different on every model, and agent frameworks each invented their own message envelope. Two things changed that.</p>\n<p>First, Anthropic donated MCP to the Linux Foundation, where it now sits under the Agentic AI Foundation (AAIF) alongside other agentic projects. Second, Google donated A2A to the Linux Foundation in mid-2025.</p>\n<p>Both protocols are now governed in the open by the same umbrella, which is the main reason the stack finally stopped moving under everyone's feet.</p>\n<h3>Vertical and horizontal</h3>\n<p>The second shift is architectural. You no longer need one protocol that does everything. You use a <strong>vertical</strong> protocol for reaching data and tools, and a <strong>horizontal</strong> protocol for agents talking to peers.</p>\n<table>\n<thead>\n<tr>\n<th>Protocol</th>\n<th>Primary role</th>\n<th>Governance</th>\n<th>Status</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>MCP</strong></td>\n<td>Agent to tool/data</td>\n<td>Linux Foundation, AAIF</td>\n<td>De-facto standard</td>\n</tr>\n<tr>\n<td><strong>A2A</strong></td>\n<td>Agent to agent</td>\n<td>Linux Foundation</td>\n<td>Stable, actively spec'd</td>\n</tr>\n<tr>\n<td><strong>ACP</strong></td>\n<td>Agent messaging</td>\n<td>Legacy (IBM / BeeAI)</td>\n<td>Merged into A2A</td>\n</tr>\n</tbody>\n</table>\n<h2>MCP: the universal adapter for tools</h2>\n<p>MCP is the vertical layer. It standardizes how an agent connects to everything that is <em>not</em> another agent: your Shopify store, your Postgres instance, a filesystem, an internal REST service.</p>\n\n  <div>A filesystem tool definition written for one specific model's function-calling format.</div>\n  <div>One server. Any MCP-compatible client connects and discovers the same capabilities.</div>\n\n<p>Any MCP-compatible client (Claude, an IDE assistant, a local model runner) connects and discovers the same capabilities without you rewriting anything.</p>\n<figure>\n<p><strong>Why MCP won the tool layer:</strong></p>\n<ul>\n<li><strong>JSON-RPC 2.0 on the wire.</strong> Boring, well-understood, easy to debug.</li>\n<li><strong>Transport flexibility.</strong> <code>stdio</code> for local servers, Streamable HTTP for remote ones. (The older HTTP+SSE transport is legacy; new remote servers should use Streamable HTTP.)</li>\n<li><strong>Three primitives, not thirty.</strong> Servers expose tools the model can call, resources it can read, and prompts the user can invoke. Clients contribute sampling and roots, and a 2025 revision added elicitation so a server can ask the user for missing input mid-call.</li>\n<li><strong>Real ecosystem.</strong> Thousands of public servers exist for common SaaS platforms, and the official MCP Registry gives clients a discovery path instead of a README scavenger hunt.</li>\n<li><strong>Scoped permissions.</strong> Servers expose only what you register, so an agent sees the slice of data it needs and nothing else.</li>\n</ul>\n<p>If you are modernizing how your systems are consumed, exposing your data through MCP is the first move with the biggest payoff. It decouples your tools from your model choice, which is the coupling that hurts most when you swap models.</p>\n<p>I went deeper on the mechanics in <a href=\"https://ansezz.com/blog/api-vs-mcp/\">API vs MCP</a> and <a href=\"https://ansezz.com/blog/mcp-context-aware-agents/\">MCP tool-use for context-aware agents</a>.</p>\n\nPick one internal API your agents already call through a custom script and wrap it in an MCP server.\n\n<h2>A2A: the social fabric for agents</h2>\n<p>MCP handles tools. A2A handles peers.</p>\n<p>When your merchandising agent needs stock levels from an inventory agent owned by another team (one built on a different framework, deployed in a different cluster), that conversation is A2A. This is the horizontal layer.</p>\n<p>A2A lets agents discover each other, delegate tasks, stream progress, and hand back results.</p>\n<h3>The Agent Card</h3>\n<p>The anchor of the protocol is the <strong>Agent Card</strong>: a JSON document, conventionally served at <code>/.well-known/agent-card.json</code>. It declares who the agent is, which skills it offers, which transports and auth schemes it supports, and what input and output types it accepts. Discovery is fetching that file.</p>\n<p><strong>What A2A gives you:</strong></p>\n<ul>\n<li><strong>Capability discovery.</strong> A client reads an Agent Card (or queries a registry) to find an agent that handles the job, instead of hard-coding a hostname.</li>\n<li><strong>Task delegation with a real lifecycle.</strong> Tasks move through explicit states (submitted, working, input-required, completed, failed, canceled), so a caller can reason about a long-running job rather than polling a black box.</li>\n<li><strong>Streaming and callbacks.</strong> Server-Sent Events for live progress on an open connection, plus webhook-style push notifications for tasks that outlive it.</li>\n<li><strong>Framework independence.</strong> The peer on the other side can be LangGraph, CrewAI, a Laravel service wrapping a model, or something homegrown. The wire format is what you agree on, not the runtime.</li>\n<li><strong>Standard auth.</strong> Agent Cards declare security schemes, so you authenticate agent-to-agent traffic with the same primitives you already use for APIs.</li>\n</ul>\n<p>This is what makes cross-team agents workable. Building <a href=\"https://ansezz.com/blog/agentic-commerce-shopify/\">agentic commerce on Shopify</a> already means a checkout flow handing structured instructions to fulfillment logic; A2A is what stops that handoff from being a private contract nobody else can reuse.</p>\n<h2>Why ACP merged into A2A</h2>\n<p>You will still hit references to the Agent Communication Protocol. ACP came out of IBM and the BeeAI team as a REST-native approach to agent messaging.</p>\n<p>It was good at exactly the thing A2A was weakest at early on: stateful, long-running, asynchronous work.</p>\n<p>The problem was arithmetic. Two competing agent-to-agent protocols meant every framework author had to pick one or implement both, which is how ecosystems stall. During 2025 the ACP maintainers moved to consolidate behind A2A under Linux Foundation governance.</p>\n<p>The practical consequence is short. ACP is no longer an architectural choice you need to evaluate. Its strong ideas around state management and durable messaging live on in A2A's task model.</p>\n<p>If you had ACP on a roadmap, substitute A2A. The concepts map closely and the community is now in one place.</p>\n<h2>Architecting the two-layer stack</h2>\n<p>The mental model that keeps this straight:</p>\n<p>MCP is the device driver, A2A is the network protocol.</p>\n<p>One lets a process talk to the hardware it is attached to; the other lets it talk to other machines. You need both, and they do not overlap.</p>\n<p>An agent is typically an A2A <strong>server</strong> to its peers and an MCP <strong>client</strong> to its tools, at the same time. That dual role is the whole architecture.</p>\n<figure>\n<p><strong>A workable implementation order:</strong></p>\n<ol>\n<li><strong>Data layer (MCP).</strong> Wrap your internal APIs and databases in MCP servers. This is the step that pays off immediately, even with a single agent, and it is where a lot of <a href=\"https://ansezz.com/blog/claude-mcp-dev-tools/\">Claude and MCP dev tooling</a> already lives.</li>\n<li><strong>Coordination layer (A2A).</strong> Put an A2A-compliant interface in front of each agent and publish its Agent Card. This is how <a href=\"https://ansezz.com/blog/agentic-workflows-vibe-coding/\">informal agentic workflows</a> graduate into services other teams can call.</li>\n<li><strong>Governance layer.</strong> Route the traffic through an <a href=\"https://ansezz.com/blog/api-gateway-ai-stack/\">AI gateway</a> so you get auth, rate limits, cost attribution, and traces on both protocols instead of guessing where the spend went.</li>\n</ol>\n<h3>Tools and peers are not interchangeable</h3>\n<p>A tool call and an agent delegation are not interchangeable, even when both are technically reachable.</p>\n<p>Exposing another team's agent as an MCP tool flattens away the task lifecycle and turns a long-running negotiation into a request that either returns or times out.</p>\n<p>Use MCP for deterministic capabilities you own, and A2A when the other side needs to reason, ask you a follow-up question, or take twenty minutes.</p>\n<h2>Practical implementation tips</h2>\n<p>Moving to a protocol-first architecture is mostly a change in what you consider the unit of work. Stop shipping \"an agent\" and start shipping services that speak these standards.</p>\n<p><strong>Audit your tools first.</strong> Pick the five tools your agents actually reach for and build or adopt MCP servers for them. Resist wrapping everything: an agent handed forty tools makes worse choices than one handed six.</p>\n<p><strong>Write Agent Cards early.</strong> Even with two agents, defining the card forces you to name each agent's skills and boundaries. Most multi-agent designs fail because responsibilities overlap, and the card is where that shows up before it becomes a runtime problem.</p>\n<p><strong>Standardize transports.</strong> Streamable HTTP for remote MCP servers, HTTP with SSE for A2A streaming. Streaming matters more than it looks: without it you cannot see intermediate reasoning or partial tool output, and debugging a multi-agent failure becomes archaeology.</p>\n<p><strong>Version the contracts, not the prompts.</strong> Agent Cards and tool schemas are public interfaces now. Treat a changed tool signature like a breaking API change, because for every agent calling it, that is exactly what it is.</p>\n<p><strong>Instrument at the boundary.</strong> Every MCP call and A2A task should emit a trace with a correlation ID that survives delegation. When a five-agent chain returns nonsense, the only cheap way to find the bad hop is a trace that spans all of them.</p>\n\nWrite an Agent Card for one agent you already run, even if nobody calls it yet. Naming its skills will show you where responsibilities overlap.\n\n<p>If you are wiring MCP and A2A into a real product, <a href=\"https://ansezz.com/services/\">here's how I help teams ship it</a>.</p>\n\n- **MCP is vertical.** It connects agents to tools, databases, and APIs, and it is the de-facto standard for that layer.\n- **A2A is horizontal.** It handles discovery, delegation, and task lifecycle between agents, across frameworks and teams.\n- **ACP is legacy.** It merged into A2A. Do not start new work on standalone ACP.\n- **Governance is settled.** Both protocols now sit under Linux Foundation stewardship, which is what makes them safe to build against.\n- **Each agent plays two roles.** A2A server to its peers, MCP client to its tools: that dual role is the stack in one sentence.\n- **The boundary matters.** Deterministic capability you own is a tool; another team's reasoning system is a peer. Mixing them up is how the architecture rots.\n\n<p>How are you handling coordination between agent frameworks in your production stack right now?</p>\n</figure></figure>",
      "date_published": "2026-08-05T00:00:00.000Z",
      "date_modified": "2026-08-05T00:00:00.000Z",
      "tags": [
        "architecture",
        "mcp",
        "agentic-ai",
        "ai",
        "architecture"
      ],
      "image": "https://ansezz.com/blog/mcp-vs-a2a-vs-acp/hero-46b5ff93.webp"
    },
    {
      "id": "https://ansezz.com/blog/coolify-2026-self-hosted-paas/",
      "url": "https://ansezz.com/blog/coolify-2026-self-hosted-paas/",
      "title": "Coolify in 2026: 60k GitHub stars and the self-hosted PaaS bet",
      "summary": "Coolify crossed 60k GitHub stars. What that buys you, how it really compares to Dokploy, CapRover and Dokku, and the firewall claim to ignore.",
      "content_html": "<p>Scaling a web app used to mean signing a blank check. Coolify is why it doesn't have to anymore.</p>\n\n- Coolify gives you a Heroku-style workflow on your own servers, built on plain Docker.\n- Dokploy is now a real competitor. Coolify's edge is time in production, not catalog size.\n- Coolify does not manage your firewall. Docker bypasses UFW, so filter at your provider instead.\n\n<p>You start on the free tier, traffic spikes, and suddenly the bill has a comma in it.</p>\n<p>The alternative (a raw VPS, SSH, and a prayer) costs you the weekend instead of the money. Nginx vhosts, certbot renewals that silently fail, a deploy script nobody wants to touch.</p>\n<p>That gap is the whole reason Coolify exists, and it's why the project has quietly turned into the default answer for \"I want Heroku, but on my server.\"</p>\n<p>As of today the repo sits at <strong>60,298 GitHub stars</strong> and ships hundreds of one-click services. Version 4 has been the stable line for a while now.</p>\n<p>It is no longer a hobby project you're brave for running. It's infrastructure with a bus factor above one.</p>\n<p>I run production workloads on it. This post is what I'd tell you over coffee: what the numbers mean, where Coolify genuinely wins, and which of its marketing-adjacent claims you should quietly ignore.</p>\n<h2>What 60k stars actually signals</h2>\n<p>Stars are a vanity metric right up until they aren't. What the curve tells you here is not \"Coolify is good software.\" It's that a critical mass of developers decided <strong>sovereign infrastructure</strong> was worth the tradeoff.</p>\n<p>The reasoning is boring and correct. A managed PaaS owns your build pipeline, your runtime, your egress pricing, and your migration path. When any one of those changes, you refactor on their timeline.</p>\n<p>Heroku killing free dynos taught a generation of developers that \"managed\" means \"someone else decides.\"</p>\n<p>Self-hosting used to mean giving up the good parts: git-push deploys, automatic TLS, a dashboard your teammate can read. Coolify's actual contribution is that it kept those and dropped the rent.</p>\n<p>Practically, a mature project means the things that bite you at 2am are handled by someone other than you: proxy config, certificate renewal, container lifecycle, backup scheduling. That's the product.</p>\n<h2>Docker containerization is the whole engine</h2>\n<p>Underneath the dashboard, Coolify is an orchestrator for Docker. Every app, database, and service you deploy is a container. Your local environment and your production server run the same image, which is the entire point.</p>\n<figure>\n<p>When you push to your repo, Coolify pulls the code, builds an image, and starts the replacement container.</p>\n<p>Build sources are flexible: a Dockerfile if you have one, a <code>docker-compose.yaml</code> for multi-service stacks, static output for a plain frontend, or <strong>Nixpacks</strong> when you want zero config and can live with its opinions.</p>\n<p>What containerization buys you here:</p>\n<ul>\n<li><strong>Isolation.</strong> One app OOM-ing doesn't take the box down with it.</li>\n<li><strong>Portability.</strong> Moving from the cheapest droplet your provider sells to a dedicated Hetzner box is a server swap, not a migration project.</li>\n<li><strong>Resource control.</strong> Per-container CPU and memory limits, set in the UI, enforced by Docker.</li>\n<li><strong>Reproducibility.</strong> The image that passed staging is the image that runs in production.</li>\n</ul>\n<h3>Where Kubernetes starts to make sense</h3>\n<p>If you're weighing this against a full orchestrator, my breakdown of <a href=\"https://ansezz.com/blog/docker-vs-kubernetes/\">Docker vs Kubernetes</a> covers where the line actually sits, and it's further out than most teams assume.</p>\n<p>Kubernetes earns its complexity somewhere north of \"a dozen services and a platform team.\" Below that, Coolify's model is strictly less work for the same outcome.</p>\n<p>For the ground-floor version of this setup, I wrote it up in <a href=\"https://ansezz.com/blog/coolify-docker-saas-hosting/\">Coolify and Docker for SaaS hosting</a>.</p>\n<h2>What the one-click service catalog actually contains</h2>\n<p>This is the part people underrate. The service catalog is not a gimmick. It's the difference between \"I'll set up Plausible this weekend\" and \"Plausible is running, next task.\"</p>\n<p>Worth splitting into two things that get conflated.</p>\n<p><strong>Databases are first-class resources</strong>, not templates. There are eight of them: PostgreSQL, MySQL, MariaDB, MongoDB, Redis, DragonFly, KeyDB, and ClickHouse. They get their own connection UI, their own lifecycle, and (for the SQL and document stores) their own backup scheduling.</p>\n<p>Everything else is a compose template, and that's where the breadth lives:</p>\n<ul>\n<li><strong>Dev tools</strong>: Gitea, Hoppscotch, n8n, Uptime Kuma.</li>\n<li><strong>Analytics</strong>: Plausible, Umami, PostHog.</li>\n<li><strong>CMS and app platforms</strong>: Ghost, Strapi, Directus, WordPress, Appwrite, Supabase.</li>\n<li><strong>AI infrastructure</strong>: Qdrant and other vector stores for RAG workloads.</li>\n</ul>\n<p>Each template ships with sane defaults, networking, volume mounts, and generated secrets. You get a running service and a URL.</p>\n<h3>Catalog size is not a moat</h3>\n<p>The \"280+\" figure is Coolify's own tagline and it undersells the current state: the repo's <code>templates/compose</code> directory holds 362 of them today.</p>\n<p>Worth knowing, though, that catalog size is no longer a moat. Dokploy ships 514 blueprints and CapRover has 354 one-click apps. Everyone in this space has enough templates.</p>\n<p>Two caveats worth stating plainly. First, a one-click template is a <em>starting point</em>. It is not tuned for your load, and the defaults are chosen to boot cleanly, not to survive traffic.</p>\n<p>Second, templates are community-maintained; pin your image tags rather than riding <code>latest</code> into a breaking upgrade.</p>\n\nOpen every Coolify service you run and replace any `latest` image tag with a pinned version.\n\n<h2>Coolify vs the alternatives, on real numbers</h2>\n<p>The self-hosted PaaS space stopped being a one-horse race. Here's where the four main projects actually stand today:</p>\n<table>\n<thead>\n<tr>\n<th>Feature</th>\n<th>Coolify</th>\n<th>Dokploy</th>\n<th>CapRover</th>\n<th>Dokku</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Interface</strong></td>\n<td>Web dashboard</td>\n<td>Web dashboard</td>\n<td>Web UI</td>\n<td>CLI-first</td>\n</tr>\n<tr>\n<td><strong>Multi-server</strong></td>\n<td>Native + Swarm</td>\n<td>Native + Swarm</td>\n<td>Docker Swarm</td>\n<td>Via plugins</td>\n</tr>\n<tr>\n<td><strong>Service catalog</strong></td>\n<td>362 templates</td>\n<td>514 blueprints</td>\n<td>354 one-click apps</td>\n<td>Plugin-based</td>\n</tr>\n<tr>\n<td><strong>Build sources</strong></td>\n<td>Dockerfile / Compose / Nixpacks / static</td>\n<td>Dockerfile / Compose / Nixpacks / Railpack / Heroku + Paketo buildpacks</td>\n<td>Dockerfile / captain-definition</td>\n<td>Herokuish buildpacks / Dockerfile</td>\n</tr>\n<tr>\n<td><strong>Backups</strong></td>\n<td>S3-compatible, scheduled</td>\n<td>S3-compatible</td>\n<td>Manual / scripted</td>\n<td>Plugin-based</td>\n</tr>\n<tr>\n<td><strong>GitHub stars</strong></td>\n<td>60.3k</td>\n<td>36.5k</td>\n<td>15.1k</td>\n<td>32.1k</td>\n</tr>\n</tbody>\n</table>\n<p>A few honest reads on that table.</p>\n<p><strong>Dokploy</strong> is the real competitor now, not a footnote. It has 36k stars and is climbing fast, with a template catalog that has quietly grown <em>larger</em> than Coolify's and a wider set of build sources (Railpack and Paketo on top of the usual Nixpacks and Dockerfile).</p>\n<p>It's leaner and the UI is arguably cleaner. If you're starting fresh in 2026 it deserves a genuine evaluation rather than a dismissal. Coolify's remaining edge is time in production: more people have hit more edge cases, and those fixes are already merged.</p>\n<p><strong>Dokku</strong> has the smallest resource footprint of the four and the most Unix-native design. If you're comfortable in a terminal and want something that will still work identically in five years, it's a defensible choice. The CLI-first model is a real barrier for teams where not everyone deploys.</p>\n<p><strong>CapRover</strong> is the oldest of the four and still actively maintained; it committed as recently as this week. It's Swarm-native by design, which is a genuine advantage if clustering is your starting requirement rather than an afterthought. Its ceiling is the smaller community: fewer people hitting problems means fewer problems already solved for you.</p>\n<h2>What multi-server actually means</h2>\n<p>Coolify supports three topologies: a single server, multiple standalone servers managed from one dashboard, and Docker Swarm clusters.</p>\n<p>The middle one is what most teams want, and it's worth being precise about what it is. Adding a second server means Coolify SSHes into it and manages Docker there. Each server runs its own containers and its own proxy.</p>\n<p>It is <strong>not</strong> a cluster. There's no automatic failover, no scheduler moving workloads between nodes. If a server dies, the apps on it are down until you redeploy elsewhere.</p>\n<p>That's a feature, not a gap. It's the model that fits the 95% case: put the database on a high-memory box, the app on a high-CPU box, and keep the Coolify instance itself on a small separate server so a runaway build can't take down your control plane.</p>\n<p>If you need true clustering, that's what the Swarm mode is for. If you need more than Swarm, you're in Kubernetes territory.</p>\n<p>I go deeper on the topology decisions, dedicated build servers, and zero-downtime rollouts in <a href=\"https://ansezz.com/blog/scaling-with-coolify/\">scaling SaaS with Coolify</a>.</p>\n<h2>Security: what's real and what isn't</h2>\n<p>This is where I'll break with the usual Coolify writeup, because there's a claim floating around that Coolify handles your firewall for you. It does not, and believing it will hurt you.</p>\n<p>Here's what Coolify actually gives you:</p>\n<ul>\n<li><strong>Automatic TLS.</strong> Let's Encrypt issuance and renewal for custom domains, handled by the bundled proxy: Traefik by default, Caddy still marked experimental.</li>\n<li><strong>Encrypted secrets.</strong> Environment variable values are stored with Laravel's <code>encrypted</code> cast, so they're not sitting in the database as plaintext. That cast uses Laravel's standard encrypted payload, the same format my <a href=\"https://ansezz.com/tools/laravel-encrypt-decrypt/\">Laravel decrypt tool</a> reads with an APP_KEY. Scopes go per-resource, with shared variables at the environment, project, and team level, and separate build-time and runtime visibility.</li>\n<li><strong>Team roles.</strong> <code>owner</code>, <code>admin</code>, and <code>member</code> per team, so a contractor doesn't get production database access.</li>\n<li><strong>Scheduled backups.</strong> Postgres gets first-class scheduled dumps (Coolify backs up its own database the same way); MySQL, MariaDB, and MongoDB are covered too. Destination is local disk or any S3-compatible bucket. Backblaze B2 and Cloudflare R2 both work and cost almost nothing.</li>\n</ul>\n<h3>The Docker and UFW trap</h3>\n<p>Here's the part nobody says out loud:</p>\n<p>Docker publishes ports by writing NAT rules that bypass UFW.</p>\n<p>If you <code>ufw deny 5432</code> and then map Postgres to the host, that port is open to the internet and <code>ufw status</code> will happily tell you it's blocked. This is Docker behavior, not a Coolify bug, but the consequence lands on you either way.</p>\n\n  <div>`ufw deny 5432` is set, and `ufw status` says the port is blocked.</div>\n  <div>Docker's NAT rules route around UFW, and Postgres is open to the internet.</div>\n\n<p>The fix is to filter at a layer Docker can't route around:</p>\n<ol>\n<li><strong>Use your provider's firewall.</strong> Hetzner Cloud Firewall, DigitalOcean Cloud Firewall, AWS security groups: these sit outside the host, so Docker's iptables rules are irrelevant.</li>\n<li><strong>Don't publish database ports at all.</strong> Databases should be reachable over the internal Docker network by container name. Coolify defaults to this; the risk is you enabling public access \"just for a migration\" and forgetting.</li>\n<li><strong>Close the dashboard ports.</strong> Coolify needs 8000, 6001, and 6002 during setup. Once you've put the dashboard behind a domain and its own TLS, those can be closed. Leave 22, 80, and 443.</li>\n</ol>\n<p>Do those three and your self-hosted setup is meaningfully harder to reach than most managed deployments, because there's less surface exposed to begin with.</p>\n\nFrom a machine outside your network, scan your server's public IP for open database ports. If 5432 answers, fix it at the provider firewall today.\n\n<h2>The actual argument: vendor independence</h2>\n<p>Strip away the features and the case for Coolify is one sentence: you stop renting your deployment pipeline.</p>\n<figure>\n<p>The math is not subtle. Ten small projects on a managed platform (a few hobby-tier databases, some build minutes, a bit of bandwidth) lands somewhere around $150 to $250/month once you're past free tiers.</p>\n<p>The same ten projects fit on a single mid-tier VPS with 8 vCPU and 16 GB of RAM, with headroom to spare. On Hetzner that's a low-double-digit euro bill on the ARM line, roughly double on x86. Either way it's an order of magnitude, not a percentage.</p>\n<h3>The catch</h3>\n<p>You are now the on-call engineer. Kernel updates, disk space, a container that wedges at 3am: that's yours. Coolify reduces that work by maybe 90%, not 100%.</p>\n<p>For a solo developer or a small team, the trade is usually worth it. For a company where an hour of downtime costs more than a year of managed hosting, it isn't, and you should pay the tax.</p>\n<p>I made this exact switch and wrote up the numbers in <a href=\"https://ansezz.com/blog/coolify-self-hosted-saas/\">why I'm ditching expensive cloud providers</a>. The runway extension was the point; the control turned out to matter more.</p>\n<h2>How I'd approach the migration</h2>\n<p>If you're considering the move, the order matters more than the tooling:</p>\n<ul>\n<li><strong>Price it honestly.</strong> Add up your current managed spend including bandwidth and build minutes, then compare against a VPS with 2x the resources you think you need. If the gap isn't at least 3x, the operational burden probably isn't worth it. My <a href=\"https://ansezz.com/tools/self-hosting-cost-calculator/\">self-hosting cost calculator</a> does that sum and prices in your ops hours.</li>\n<li><strong>Start with something you can afford to break.</strong> Staging environments, internal dashboards, side projects. Learn the failure modes on workloads where the blast radius is you.</li>\n<li><strong>Get containerized first.</strong> If your app already builds from a Dockerfile, migration is an afternoon. If it doesn't, fix that <em>before</em> you touch Coolify; you'll be debugging one thing instead of two.</li>\n<li><strong>Wire up S3 backups on day one.</strong> Before the first production workload lands. A single server with no offsite backup is a countdown, not an architecture.</li>\n<li><strong>Put the control plane on its own box.</strong> Coolify's documented minimum is 2 cores, 2 GB of RAM, and 10 GB of free disk, which the cheapest tier most providers sell already covers. A dedicated instance means a runaway build can never take down the dashboard you'd use to fix it.</li>\n<li><strong>Add external monitoring.</strong> Coolify tells you a container is running. It does not tell you your users can check out. Uptime Kuma (one-click, naturally) or Better Stack closes that gap.</li>\n</ul>\n<p>Coolify's real achievement is that it made the PaaS experience a commodity. The dashboard, the git integration, the automatic TLS: these were competitive moats five years ago.</p>\n<p>Now they're a <code>curl | bash</code> away, and the only thing you're paying a managed provider for is not thinking about servers. That's a legitimate thing to pay for. It just isn't worth a 10x premium anymore.</p>\n\n- Coolify gives you a PaaS experience on servers you own, built on plain Docker.\n- Compare it honestly with Dokploy, CapRover and Dokku. Catalog size no longer decides it.\n- Multi-server is not a cluster. Use Swarm if you need real clustering.\n- Filter ports at your provider's firewall, because Docker bypasses UFW.\n- Price the move honestly, and remember you become the on-call engineer.\n\n<p>What's still keeping you on managed hosting: the operational load, or the fear of being the one holding the pager? <a href=\"https://ansezz.com/contact/\">Tell me</a> which one it is.</p>\n</figure></figure>",
      "date_published": "2026-08-03T00:00:00.000Z",
      "date_modified": "2026-08-03T00:00:00.000Z",
      "tags": [
        "devops",
        "coolify",
        "self-hosting",
        "devops",
        "docker",
        "deployment"
      ],
      "image": "https://ansezz.com/blog/coolify-2026-self-hosted-paas/hero-41f1dd71.webp"
    },
    {
      "id": "https://ansezz.com/blog/ai-coding-workflow-levels/",
      "url": "https://ansezz.com/blog/ai-coding-workflow-levels/",
      "title": "AI coding is like an addiction (in the best way)",
      "summary": "A level-by-level map of the AI coding workflow: gateway prompts, codebase RAG, then multi-agent orchestration over MCP, and why manual coding now drags.",
      "content_html": "<p>AI coding is strangely addictive.</p>\n\n- AI coding grows in levels: gateway prompts, then codebase context and RAG, then multi-agent loops over MCP.\n- Context is what changes the output. Specs, tests and pgvector memory beat clever prompts.\n- Split the agents so the one writing the code is never the only one grading it.\n\n<p>You start with a single prompt to fix a regex or center a div. You tell yourself it's a one-time thing to save ten minutes.</p>\n<p>Then you watch it refactor an entire <strong>Laravel</strong> controller in seconds. Suddenly you're asking it to write your unit tests. Before long you're building custom agents that manage a <strong>Shopify</strong> store over <strong>GraphQL</strong>.</p>\n<p>The shift happens slowly at first. Then all at once.</p>\n<h2>The real problem is blank-page fatigue</h2>\n<p>The problem it solves for most of us isn't typing speed. It's blank-page fatigue. Staring at a legacy codebase or a fresh architecture doc feels like a mountain of manual labor.</p>\n<p>You burn hours on boilerplate instead of business logic. That friction is where motivation goes to die.</p>\n<p>And the cost of not evolving compounds. Coding without AI in 2026 feels like writing a book with a quill while everyone else runs a word processor. It isn't only speed. It's <strong>cognitive load</strong>. Energy spent on syntax and API nuance is energy not spent on architecture and strategy.</p>\n<p>The answer isn't \"use AI.\" It's building a structured <strong>AI coding workflow</strong>: moving from passive user to engineer who orchestrates systems. Once you've felt a multi-agent loop close on its own, there's no going back.</p>\n<h2>Levels 1 to 5: the gateway prompts</h2>\n<p>Almost everyone starts here. You treat the model like an interactive Stack Overflow. Paste a snippet, ask for a fix, move on.</p>\n<p>At <strong>Level 1</strong> you're generating small functions and boilerplate. At <strong>Level 5</strong> you're debugging. Instead of manually scanning logs, you drop a stack trace into a chat and get a diagnosis. That alone saves real hours.</p>\n<p>But you're still stuck in the copy-paste loop. You write the code. The AI patches the errors. It's a helpful assistant that has no idea what you're building.</p>\n<p>The trap at this level is letting the model freestyle without context. Escaping it means writing specs instead of wishes. You stop saying \"fix this\" and start saying something like the right side here:</p>\n\n  <div>\"Fix this.\"</div>\n  <div>\"Refactor this Laravel service to follow the Repository pattern, keep the existing public method signatures, and make sure database exceptions surface as domain exceptions.\"</div>\n\n<p>Same model. Completely different output.</p>\n<h2>Levels 10 to 20: building context</h2>\n<p>At this stage you realize the model is only as good as the information it can see. Isolated chat windows stop being enough.</p>\n<p>You start using tools that read your whole codebase. This is the point where the <a href=\"https://ansezz.com/blog/ai-vs-traditional-development/\">AI vs traditional development</a> question stops being philosophical and turns into a workflow decision.</p>\n<p><strong>Level 10</strong> is tests. Writing unit tests is usually the chore you postpone; AI makes it fast. A full <strong>Pest</strong> or <strong>PHPUnit</strong> suite for a Laravel feature drops in seconds.</p>\n<p>You're not just producing code anymore. You're producing a safety net that lets the agents at higher levels move fast without wrecking things.</p>\n<p><strong>Level 20</strong> is when the AI actually knows your codebase. Codebase indexing, embeddings, RAG. The model retrieves the three files that matter instead of guessing from the file tree.</p>\n<p>Many teams go further and stand up a local <strong>pgvector</strong> database holding internal docs, ADRs, and runbooks. It's the same retrieval discipline behind the <a href=\"https://ansezz.com/blog/7-rag-mistakes-production/\">7 mistakes wrecking your production RAG stack</a>.</p>\n<p>The payoff is that you stop re-explaining your architecture in every session. Your event-driven layer, your custom service container bindings, your weird legacy billing table: the agent looks it up.</p>\n<figure>\n<h2>Level 50+: the agentic shift</h2>\n<p>This is where the addiction matures into an engineering practice. You're no longer chatting with a model. You're building systems that act on your behalf.</p>\n<p><strong>Level 50</strong> is the <a href=\"https://ansezz.com/blog/claude-mcp-dev-tools/\">Model Context Protocol</a>. MCP lets agents connect to real tools: the Shopify Admin GraphQL API, your database, your cloud infrastructure.</p>\n<p>It's the same plumbing behind <a href=\"https://ansezz.com/blog/agentic-commerce-shopify/\">agentic commerce on Shopify</a>, pointed at your own workflow instead of at a shopper. An agent can watch incoming orders and adjust inventory according to rules you described in plain language, because it holds an actual connection instead of a description of one.</p>\n<p>The Level 50 loop I actually run looks like this:</p>\n<ol>\n<li><strong>Plan.</strong> You write a technical spec in markdown: scope, constraints, acceptance criteria.</li>\n<li><strong>Execute.</strong> An agent reads the spec and generates the Laravel migrations, models, and controllers.</li>\n<li><strong>Verify.</strong> A <em>separate</em> agent writes tests and runs them until they pass.</li>\n<li><strong>Deploy.</strong> A deployment agent ships to a <a href=\"https://ansezz.com/blog/coolify-self-hosted-saas/\">Coolify instance</a> and tails the logs for errors.</li>\n</ol>\n<p>You become the conductor. You own the <em>what</em> and the <em>why</em>; the agents grind through the <em>how</em>.</p>\n<h3>Why the separate verifier matters</h3>\n<p>The separation in step 3 matters more than it looks.</p>\n<p>An agent that writes both the implementation and its tests will happily write tests that pass against its own bug.</p>\n<p>Two agents with different context catch what one agent rationalizes away.</p>\n<p>That is why I stopped reading diffs line by line and moved to a <a href=\"https://ansezz.com/blog/stop-reading-code-ai-review/\">multi-agent review pipeline</a>, and why <a href=\"https://ansezz.com/blog/testing-ai-generated-code/\">AI-generated code still needs its own test discipline</a>.</p>\n<figure>\n<h2>Why manual coding feels slow now</h2>\n<p>Once you've run an agentic workflow, the friction of manual coding becomes physically annoying. Five-minute tasks feel like an eternity.</p>\n<p>Hunting the correct GraphQL mutation shape for a Shopify app feels absurd when an agent with schema access resolves it in two seconds.</p>\n<p>The addiction isn't to the tool. It's to <strong>flow</strong>. AI removes the micro-frictions that shatter concentration: the doc lookup, the typo, the forgotten import, the \"wait, what's this method called again.\" You stay in a high-level creative state for longer stretches, so you solve bigger problems.</p>\n<p>It also quietly redefines what senior means. Not knowing every syntax detail from memory. Knowing how to decompose a system, and how to steer an AI into building it correctly. Builder to architect.</p>\n<h2>Practical steps to level up</h2>\n<p>You don't drift into Level 50. You have to build the infrastructure for it.</p>\n<h3>1. Master MCP</h3>\n<p>Start with custom MCP servers for the tasks you repeat weekly. If you work on Shopify apps, expose a tool that queries your store's Admin GraphQL API directly, and another that validates a mutation against the current schema version.</p>\n<p>That single step kills most of the copy-paste in your day. Keep the surface small. Every extra tool is context tax and one more thing the agent can misuse.</p>\n\nPick one task you repeat every week and wrap it in a single small MCP tool. Use it for a week before adding a second one.\n\n<h3>2. Implement RAG over your codebase</h3>\n<p>Don't rely on general knowledge for project-specific questions. Index your project.</p>\n<p>If you're running a Laravel SaaS, make sure the agent can retrieve your service-layer conventions and your event contracts, not just guess from <code>app/</code>. Storing documentation embeddings in <strong>pgvector</strong> is the cheapest long-term memory you'll ever buy, and it lives in the Postgres you already run.</p>\n<h3>3. Use multi-agent orchestration</h3>\n<p>Stop thinking about one AI and start thinking about a team. Planner, implementer, reviewer.</p>\n<p>Separation of concerns produces better code and fewer hallucinations, for the same reason it does with humans: the reviewer isn't emotionally invested in the plan.</p>\n<h3>4. Optimize for Shopify GraphQL</h3>\n<p>Shopify has moved on from REST for new development. Make sure your agents target the current <strong>GraphQL Admin API</strong> version and don't reach for deprecated fields they memorized from a 2023 blog post.</p>\n<p>Pin the API version explicitly in your prompts and your client. Schema drift is the single most common source of confidently wrong Shopify code.</p>\n<pre><code>// Example of a Laravel service using a Shopify GraphQL mutation.\n// An agent writes the boilerplate; your spec dictates the contract.\npublic function updateProductInventory(string $inventoryItemId, int $newQuantity)\n{\n    $mutation = &lt;&lt;&lt;'GRAPHQL'\n    mutation inventorySetQuantities(\n      $input: InventorySetQuantitiesInput!\n      $idempotencyKey: String!\n    ) {\n      inventorySetQuantities(input: $input) @idempotent(key: $idempotencyKey) {\n        inventoryAdjustmentGroup {\n          createdAt\n          reason\n          changes {\n            name\n            delta\n            quantityAfterChange\n          }\n        }\n        userErrors {\n          code\n          field\n          message\n        }\n      }\n    }\n    GRAPHQL;\n\n    $variables = [\n        'idempotencyKey' =&gt; (string) \\Illuminate\\Support\\Str::uuid(),\n        'input' =&gt; [\n            'name' =&gt; 'available',\n            'reason' =&gt; 'correction',\n            // Required on current Admin API: compare-and-set. null skips the check.\n            'quantities' =&gt; [\n                [\n                    'inventoryItemId' =&gt; $inventoryItemId,\n                    'locationId' =&gt; config('shopify.default_location_id'),\n                    'quantity' =&gt; $newQuantity,\n                    'changeFromQuantity' =&gt; null,\n                ],\n            ],\n        ],\n    ];\n\n    return $this-&gt;shopifyClient-&gt;query($mutation, $variables);\n}\n</code></pre>\n<h2>What schema drift breaks</h2>\n<p>Two details in there are exactly the kind of thing schema drift breaks.</p>\n<p>The <code>@idempotent</code> directive arrived as optional in API version <code>2026-01</code> and became <strong>required</strong> in <code>2026-04</code>. Any agent working from a 2024 blog post will emit this mutation without it and get rejected outright.</p>\n<p>Worse, a model that half-remembers the shape will reach for <code>inventorySetQuantity</code> (singular), which isn't a mutation in the current schema at all.</p>\n<p>And note <code>userErrors</code>. Shopify returns a <code>200</code> with a populated <code>userErrors</code> array for most business-rule failures, so an agent that only checks HTTP status will report success on a write that silently did nothing.</p>\n<p>Bake both rules into your spec once and every generated mutation inherits them.</p>\n\nAdd two lines to your agent spec: \"always send `@idempotent` with a key\" and \"treat a non-empty `userErrors` as a failure.\" Then regenerate one mutation and compare.\n\n\n- **Start small, aim high.** Level 1 prompts are a doorway, not a destination. The value lives in the loops.\n- **Context is king.** RAG and pgvector give the model specific knowledge of your codebase and business rules instead of internet averages.\n- **Embrace MCP.** Real tool connections (Shopify GraphQL, your database, your infrastructure) beat pasted screenshots every time.\n- **Split the agents.** Whoever writes the code should not be the only one grading it.\n- **Standardize the loop.** Plan, execute, verify, deploy. The structure is what keeps quality from collapsing as speed goes up.\n- **Focus on architecture.** As implementation gets cheap, your value moves to design, constraints, and judgment.\n\n<p>The hardest part of AI coding isn't the learning curve. It's realizing you can't go back. You've tasted a different level of efficiency and the old way now looks like manual labor.</p>\n<p>This isn't about replacing yourself. It's about amplifying yourself until you're doing the work of a team.</p>\n<p>If you want to compare agent stacks, <a href=\"https://ansezz.com/contact/\">get in touch</a>.</p>\n<p><strong>How are you structuring your AI coding workflow beyond simple chat prompts?</strong></p>\n</figure></figure>",
      "date_published": "2026-08-01T00:00:00.000Z",
      "date_modified": "2026-08-01T00:00:00.000Z",
      "tags": [
        "ai",
        "shopify",
        "laravel",
        "code-quality",
        "vibe-coding",
        "pgvector",
        "claude",
        "mcp"
      ],
      "image": "https://ansezz.com/blog/ai-coding-workflow-levels/hero-26647505.webp"
    },
    {
      "id": "https://ansezz.com/blog/stop-reading-code-ai-review/",
      "url": "https://ansezz.com/blog/stop-reading-code-ai-review/",
      "title": "Stop reading every line of AI-generated code",
      "summary": "Line-by-line review breaks on agent-sized diffs. How I moved to specs, property tests, and a multi-agent review pipeline, and the 3% I still read.",
      "content_html": "<p>Nobody wants to read a 5,000-line diff.</p>\n\n- Human review works in chunks of a few hundred lines. Agent diffs blow past that, so line-by-line reading turns into theater.\n- Move the gate to specs, property tests, contract tests and machine-checked rules, then let narrow review agents run in CI.\n- I still open migrations, auth and permissions, money paths, new seams and anything nobody asked for.\n\n<p>But that is what lands in the pull request when an agent spends an afternoon refactoring a legacy service or scaffolding a new Laravel module end to end.</p>\n<p>The failure mode that follows is worth naming precisely, because it does not look like failure while it is happening.</p>\n<p>An agent scaffolds a multi-tenant module in one pass: migrations, models, policies, jobs, tests. The diff is correct, mostly. You spend two evenings reading it line by line and approve it.</p>\n<p>Weeks later something breaks in production: a queued job that ran outside the tenant scope. You read that file. You read that exact method. Your eyes went over it and your brain filed it as \"looks like the other ones.\"</p>\n<p>It feels like diligence. It is theater.</p>\n<h2>Where line-by-line review actually breaks</h2>\n<p>Code review guidance has been consistent for two decades: reviewers find defects reliably in chunks of roughly 200 to 400 lines, and effectiveness drops sharply past that.</p>\n<p>Nothing about that number has changed because we bolted an LLM onto the front of the process. Human attention is the constant. The volume of code is the variable, and it just went up by an order of magnitude.</p>\n<p>Three things go wrong when you push past that limit:</p>\n<ul>\n<li><strong>You review syntax, not semantics.</strong> Naming, formatting, and shape are cheap to evaluate, so tired brains drift toward them. A perfectly formatted diff can still ship an N+1 query, a missing index, or a broken state machine.</li>\n<li><strong>You lose the cross-file thread.</strong> The bug is almost never in the file you are reading. It is in the interaction between the job, the observer, and the tenant scope: three files you looked at twenty minutes apart.</li>\n<li><strong>You pattern-match against yourself.</strong> Agent-generated code is internally consistent. That consistency reads as correctness. Every file looks like the last one, so your reviewer instinct stops firing.</li>\n</ul>\n<p>None of this is new. What changed is that we used to write code slowly enough that review capacity roughly matched output.</p>\n<p>That coupling is gone. Generation scales with tokens; reading scales with eyeballs.</p>\n<figure>\n<h2>What agents do better, and what they don't</h2>\n<p>A review agent pointed at a repository does not read files the way you do. Given the right tooling it walks the AST, follows call graphs, and traces a value from a controller through a service into a model and out to an external API.</p>\n<p>It does that for file forty with the same attention it gave file one.</p>\n<p>That is the real advantage: <strong>breadth and consistency</strong>, not intelligence. An agent will happily check all 340 controller actions for a missing authorization call. You will check the twelve you remember.</p>\n<p>I want to be equally clear about the other side, because \"agents review better than humans\" is a bad summary of what I actually believe:</p>\n<ul>\n<li>Agents hallucinate findings. They will confidently flag a race condition that cannot occur, and the fix they suggest can be worse than the code.</li>\n<li>Agents have no idea whether the feature should exist. Product intent is not in the diff.</li>\n<li>Agents are weak on the thing that only shows up at runtime under load: the query that is fine with 200 rows and fatal with 2 million.</li>\n<li>An agent reviewing code written by the same model family shares its blind spots.</li>\n</ul>\n<p>So the move is not \"trust the agent instead of your eyes.\" It is: give the agent the work that scales with volume, and keep for yourself the work that scales with judgment.</p>\n<p>That split is the same one I drew in <a href=\"https://ansezz.com/blog/ai-vs-traditional-development/\">AI vs traditional development</a>. The boundary moved; it did not disappear.</p>\n<table>\n<thead>\n<tr>\n<th>Dimension</th>\n<th>Human line-by-line review</th>\n<th>Agentic verification</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Effective scope</strong></td>\n<td>A few hundred lines before quality drops</td>\n<td>Whole repository, via AST and call-graph traversal</td>\n</tr>\n<tr>\n<td><strong>Consistency</strong></td>\n<td>Degrades with fatigue and diff size</td>\n<td>Same checks applied to file 1 and file 400</td>\n</tr>\n<tr>\n<td><strong>Typical focus</strong></td>\n<td>Style, naming, local logic</td>\n<td>Invariants, contracts, types, test outcomes</td>\n</tr>\n<tr>\n<td><strong>Failure mode</strong></td>\n<td>Misses cross-file interactions, rubber-stamps</td>\n<td>Confident false positives, misses product intent</td>\n</tr>\n<tr>\n<td><strong>Cycle time</strong></td>\n<td>Hours to days</td>\n<td>Minutes, on every push</td>\n</tr>\n</tbody>\n</table>\n<h2>The shift: own the spec, not the syntax</h2>\n<p>If you are not reading the code, something else has to hold the line. For me that is the specification layer, and writing it now pays off more than anything else I do on an agent-assisted project.</p>\n<p>Concretely, that means:</p>\n<ul>\n<li><strong>Executable acceptance criteria.</strong> Not a Notion doc. Feature tests that fail before the agent starts and pass when it is done.</li>\n<li><strong>Property-based tests for anything with arithmetic or state.</strong> Pricing, proration, inventory, refunds. Instead of three examples, assert the invariant: a refund never exceeds the captured amount, total allocated inventory never exceeds stock on hand.</li>\n<li><strong>Contract tests at every boundary.</strong> Webhook payloads, third-party API responses, queue message shapes. This is where agent code fails quietly, because the happy path is the only path in the training data.</li>\n<li><strong>Machine-checkable rules for the things I keep repeating.</strong> Static analysis at a strict level, an architecture-test suite asserting \"no job may run without a tenant context,\" a lint rule for direct DB access outside repositories.</li>\n</ul>\n<h3>Turn repeated comments into rules</h3>\n<p>That last one is the real trick. Every review comment I would type twice becomes a rule instead.</p>\n<p>If the agent can violate an invariant and the pipeline stays green, that is my bug, not the agent's.</p>\n<p>The numbers behind why this layer is non-negotiable, and the CI configuration I run, are in <a href=\"https://ansezz.com/blog/testing-ai-generated-code/\">test AI code too</a>.</p>\n<p>When a check fails, I do not hand-patch the file. I fix the prompt, tighten the spec, or add the missing rule, then re-run.</p>\n<p>That loop is the practical core of <a href=\"https://ansezz.com/blog/agentic-workflows-vibe-coding/\">agentic workflows</a>, and it only works if the feedback the agent receives is precise. Vague context produces vague code, the point I made in <a href=\"https://ansezz.com/blog/prompt-engineering-vs-context-engineering/\">prompt engineering vs context engineering</a>.</p>\n\nScroll through your last ten review comments. Any comment you wrote twice is a candidate for a test, a static-analysis level or an architecture test.\n\n<h2>A multi-agent review pipeline that earns its cost</h2>\n<p>One giant \"review this PR\" prompt produces mush. Narrow agents with narrow briefs produce findings you can act on.</p>\n<p>This is the verify stage of the <a href=\"https://ansezz.com/blog/ai-coding-workflow-levels/\">plan, execute, verify, deploy loop</a>, and it deserves its own agents for the same reason it deserves its own step. The setup I run now has three, and they all execute inside CI rather than in my editor:</p>\n<ol>\n<li><strong>Correctness.</strong> Runs the suite, static analysis, and the architecture tests. Then reviews the diff strictly against the written acceptance criteria; its brief explicitly forbids style commentary. Output: does this satisfy the spec, and where does it not.</li>\n<li><strong>Security.</strong> Injection paths, authorization gaps on new routes, secrets in config, unsafe deserialization, mass-assignment on new models, anything touching a webhook signature. This one gets the OWASP-shaped checklist and nothing else.</li>\n<li><strong>Regression.</strong> Diffs behavior against the previous stable build. It reads the migration files, flags schema changes without a rollback path, hunts for queries added to hot code paths, and cross-references the touched files against past incident history.</li>\n</ol>\n<p>Each agent needs real access to be useful: the schema, the logs, the actual test output, not a pasted snippet. That is exactly the problem <a href=\"https://ansezz.com/blog/claude-mcp-dev-tools/\">MCP servers</a> solve, and it is the difference between a reviewer that guesses and one that checks.</p>\n<h3>Two rules that keep it quiet</h3>\n<p>First, findings are severity-ranked and anything below \"would break in production\" goes to a comment, never a block.</p>\n<p>Second, a second agent tries to <em>refute</em> each finding before I see it. A large share of the initial findings die there, and they are exactly the ones that would otherwise burn an afternoon.</p>\n<figure>\n<h2>The loop</h2>\n<p>The pipeline is only worth building if it closes without me in the middle of it:</p>\n<pre><code>[Spec + tests] --&gt; [Agent generates] --&gt; [Static analysis + type checks]\n                          ^                          |\n                          |                          v\n                   [Self-correction] &lt;-- [Test suite + review agents]\n                          |\n                          v (all green)\n                   [Human: seams review] --&gt; [Merge / deploy]\n</code></pre>\n<p>Failures feed back into the agent's context automatically. I only see the branch once it is green.</p>\n<p>That is the same discipline that makes <a href=\"https://ansezz.com/blog/ci-vs-cd/\">CI and CD</a> work in the first place: the pipeline is the gate, not the reviewer's inbox.</p>\n<h2>What I still read myself</h2>\n<p>\"Stop reading code\" is a slogan, and taken literally it is wrong. Here is my actual list of things I open every time, no matter how green the pipeline is:</p>\n<ul>\n<li><strong>Migrations.</strong> Anything irreversible or lock-heavy on a large table.</li>\n<li><strong>Auth and permission boundaries.</strong> Policies, scopes, middleware, anything that decides who sees what.</li>\n<li><strong>Money paths.</strong> Charges, refunds, discounts, tax, billing webhooks. Tests are necessary here and not sufficient.</li>\n<li><strong>New architectural seams.</strong> A new queue, a new external dependency, a new cache layer. The code may be fine; the decision may not be.</li>\n<li><strong>Anything the agent changed that nobody asked it to change.</strong> Unrequested scope is the loudest smell in agent diffs.</li>\n</ul>\n<p>That list is maybe 3% of a large diff. Reading 3% carefully beats skimming 100%, and it is the part where my decade of production scars actually transfers.</p>\n<p>The rest (the 340 controller actions, the DTO fields, the test scaffolding) is machine work, and I stopped pretending otherwise.</p>\n\nOn your next large agent PR, open only the files on that list. Note how much of the diff that covers.\n\n\n- **Line-by-line review does not scale to agent-sized diffs.** Past a few hundred lines you are performing diligence, not doing it.\n- **Use agents for breadth, not judgment.** Cross-file tracing and repeated checks are their edge; product intent and architectural taste are not.\n- **Convert review comments into rules.** Anything you would say twice belongs in a test, a static-analysis level, or an architecture test.\n- **Split the review agent into narrow briefs** (correctness, security, regression) and make a second pass try to refute each finding before it reaches you.\n- **Keep a short human read list:** migrations, auth, money, new seams, unrequested changes.\n\n<p>The uncomfortable question is what your team's review process is actually for. If it exists to catch defects, most of it should be automated by now. If it exists to spread knowledge, say that out loud and redesign it around that goal instead.</p>\n<p>Where does your review process still depend on someone reading every line, and what would it take to turn that into a check? Tell me via <a href=\"https://ansezz.com/contact/\">contact</a>, I collect these. 🤘</p>\n</figure></figure>",
      "date_published": "2026-07-30T00:00:00.000Z",
      "date_modified": "2026-07-30T00:00:00.000Z",
      "tags": [
        "ai",
        "code-quality",
        "agentic-ai",
        "ci-cd",
        "architecture"
      ],
      "image": "https://ansezz.com/blog/stop-reading-code-ai-review/hero-011470b0.webp"
    },
    {
      "id": "https://ansezz.com/blog/vibe-coding-vs-agentic-engineering/",
      "url": "https://ansezz.com/blog/vibe-coding-vs-agentic-engineering/",
      "title": "Vibe coding vs agentic engineering: which ships?",
      "summary": "Vibe coding is for prototypes, agentic engineering is for production. The daily split I use to ship AI-written code without hidden technical debt.",
      "content_html": "<p>The era of manual syntax is over.</p>\n\n- Vibe coding is for discovery: fast prototypes and spikes whose code you throw away.\n- Agentic engineering is for production: a written spec, tests and agents reviewing each other's diffs before anything merges.\n- Split the day between the two, and never let the morning branch merge.\n\n<p>In 2026 the question is no longer whether AI writes your code. It's how you govern the AI that does.</p>\n<p>Most developers I talk to are caught between two modes: the high-speed thrill of vibe coding and the slower discipline of agentic engineering.</p>\n<p>Both are real. Both work. But they don't work for the same job.</p>\n<p>Vibe coding will conjure a working application in an afternoon. Agentic engineering is what makes that application survive contact with production. Shipping in 2026 means knowing which one you're in at any given moment, and stopping the vibes before they reach <code>main</code>.</p>\n<h2>The rise and fall of vibe coding</h2>\n<p>Vibe coding was the breakout habit of 2025. It's a conversational approach where the feel of the interaction takes precedence over the underlying architecture.</p>\n<p>You describe a feature, the model returns a large block of code, you run it to see whether it works. It breaks, you paste the stack trace back, you ask for a fix. Repeat.</p>\n<p>That cycle is fast. It's addictive. It feels like magic right up until you try to scale a system built entirely on vibes.</p>\n<p>The real problem isn't the code quality; models are good now. The problem is that vibe coding is <strong>development without verification</strong>.</p>\n<p>When the model hands you 500 lines and you ship because it looks right, you're operating on faith.</p>\n<p>You've moved the bottleneck from typing to reviewing, and then you skipped the reviewing.</p>\n<h3>Where it still helps</h3>\n<p>None of that makes it useless. The first pass of a Shopify storefront section or a Laravel reporting dashboard can come together in an afternoon this way.</p>\n<p>The trouble shows up six weeks later: no tests, three competing patterns for the same problem, and a <code>Controller</code> doing the job of a service, a job, and a repository.</p>\n<p>That's not a code problem, it's a process problem. It's the same failure mode I wrote about in <a href=\"https://ansezz.com/blog/vibe-coding/\">why projects need more than just logic</a>.</p>\n<h2>Agentic engineering: the architecture of intent</h2>\n<p>Agentic engineering is the professional evolution of the same tooling. It isn't chatting with a single model. It's running a system of agents that plan, implement, test, and review under your supervision. Your role changes from coder to architect of intent.</p>\n<p>In an agentic workflow you supply a specification, not a vibe: the requirement, the architectural constraints, the acceptance criteria.</p>\n<p>The system decomposes it. One agent researches the API surface. One writes the implementation. One writes the tests. One reads the diff looking for reasons to reject it.</p>\n<p>The spec is the whole trick, and it's usually shorter than people expect:</p>\n<pre><code>## Task: subscription pause endpoint\n\nConstraints\n\n- Laravel 12, existing `SubscriptionService`, no new packages\n- Idempotent: repeat calls with the same `Idempotency-Key` return the first result\n- Authorization via existing `SubscriptionPolicy@pause`\n\nDone when\n\n- Feature test: pause, double-pause, unauthorized, already-cancelled\n- No changes to `routes/api.php` ordering\n- `php artisan test` and `vendor/bin/pint --test` both green\n</code></pre>\n<p>You're no longer reviewing lines, a habit that <a href=\"https://ansezz.com/blog/stop-reading-code-ai-review/\">stops scaling at agent-sized diffs</a>. You're reviewing a pull request that other agents already argued about.</p>\n<p>The human decision shrinks to go/no-go, which is exactly where a human decision is worth the most.</p>\n<p>That's the architecture side of it, and I go deeper into the MCP and loop mechanics in <a href=\"https://ansezz.com/blog/agentic-workflows-vibe-coding/\">the shift to agentic workflows</a>.</p>\n<figure>\n\nTake the next feature you'd normally vibe and write the spec first: constraints and a \"done when\" list. Keep it under 15 lines.\n\n<h2>Side by side: vibe coding vs agentic engineering</h2>\n<p>Most AI-assisted projects that stall share one root cause: vibe coding applied to a problem that needed agentic engineering.</p>\n<table>\n<thead>\n<tr>\n<th>Dimension</th>\n<th>Vibe coding</th>\n<th>Agentic engineering</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Primary goal</strong></td>\n<td>Speed and exploration</td>\n<td>Correctness and scale</td>\n</tr>\n<tr>\n<td><strong>Your role</strong></td>\n<td>Conversational partner</td>\n<td>Architect, mission control</td>\n</tr>\n<tr>\n<td><strong>Verification</strong></td>\n<td>\"Does it seem to work?\"</td>\n<td>Automated tests plus peer-agent diff review</td>\n</tr>\n<tr>\n<td><strong>Structure</strong></td>\n<td>Loose, improvised</td>\n<td>Written specs, repeatable SOPs</td>\n</tr>\n<tr>\n<td><strong>Best for</strong></td>\n<td>Prototypes, spikes, one-offs</td>\n<td>Production SaaS, Shopify Plus apps</td>\n</tr>\n<tr>\n<td><strong>Tech debt</strong></td>\n<td>High and invisible</td>\n<td>Managed and visible</td>\n</tr>\n<tr>\n<td><strong>Failure mode</strong></td>\n<td>Silent wrongness</td>\n<td>Slow starts, over-specification</td>\n</tr>\n</tbody>\n</table>\n<p>Note the last row. Agentic engineering has its own failure mode: you can spend an hour writing a spec for something you could have vibed in ten minutes and thrown away.</p>\n<p>Discipline applied to a throwaway prototype is just waste with better manners.</p>\n<h2>The hybrid workflow: mornings vibe, afternoons engineer</h2>\n<p>The people shipping fastest in 2026 don't pick a side. They split the day. Creative exploration in the high-energy hours, engineering rigor in the disciplined ones.</p>\n<p>If you want the rung-by-rung version of how a workflow gets there, I mapped <a href=\"https://ansezz.com/blog/ai-coding-workflow-levels/\">the levels from gateway prompts to agentic systems</a> separately.</p>\n<h3>Morning: the vibe</h3>\n<p>Start with exploration. If I'm adding a RAG-backed feature, I don't open a spec document. I open the editor and vibe out the retrieval logic.</p>\n<p>Try pgvector, try a hosted index, see how the chunking feels against real documents. Wire <a href=\"https://ansezz.com/blog/claude-mcp-dev-tools/\">Claude and MCP</a> into the local environment and let it poke at the database directly.</p>\n<p>The output of the morning is not code you keep. It's a decision you keep. Nothing from this session is allowed to merge.</p>\n<h3>Afternoon: the rigor</h3>\n<p>Once the shape is proven, the vibes end. The prototype becomes the input to a spec: schema, API contract, error semantics, test coverage.</p>\n<p>Then the agents refactor that morning code into something documented and tested, and a review agent tries to break it.</p>\n<p>The hard rule that makes this work: <strong>the morning branch never merges</strong>. It gets read, summarized into a spec, and deleted.</p>\n<p>If you let vibe-coded code merge \"just this once,\" you've quietly reverted to mode one, and today's speed becomes next month's outage.</p>\n\n  <div>Exploration code. Proves the shape, gets read, summarized into a spec, and deleted.</div>\n  <div>Spec-driven code. Documented, tested, attacked by a review agent, then merged.</div>\n\n<h2>Building the agentic stack on Laravel and Shopify Plus</h2>\n<p>Agents need predictable ground to stand on. My stack is Laravel for application logic and Shopify Plus when the surface is commerce. That's not nostalgia. It's because both are legible to a model.</p>\n<p>Laravel's conventions are the point. Service providers, policies, form requests, and a canonical directory layout mean an agent can answer \"where does this belong?\" without guessing.</p>\n<p>Shopify's GraphQL Admin API is similarly self-describing: typed, introspectable, versioned. An agent can discover the mutation it needs instead of hallucinating a REST route that was deprecated two versions ago.</p>\n<p>That predictability is most of what makes <a href=\"https://ansezz.com/blog/agentic-commerce-shopify/\">agentic commerce on Shopify</a> tractable.</p>\n<h3>Give the agents memory</h3>\n<p>The other half is memory. Your agents need your codebase, your ADRs, and your incident history. Otherwise every session starts from zero and you re-litigate decisions you settled in March.</p>\n<p>A vector store over your own repository and docs is the cheapest win in the stack, provided the retrieval layer is honest.</p>\n<p>Garbage context produces confident garbage code, which is exactly the failure pattern behind most <a href=\"https://ansezz.com/blog/7-rag-mistakes-production/\">RAG mistakes in production</a>.</p>\n<figure>\n<h2>Deploying with confidence</h2>\n<p>The last place vibes leak into production is the deploy. If a human is manually clicking ship after an agent wrote the change, the human is the weakest link. They're approving a diff they didn't write against tests they didn't read.</p>\n<p>Push the verification into the pipeline instead: migrations checked, feature tests green, a smoke pass against a preview environment, error rate watched for a window after rollout, automatic rollback if it moves.</p>\n<p>Docker plus Coolify gets you this on your own boxes without a platform bill, which is the whole premise of <a href=\"https://ansezz.com/blog/coolify-self-hosted-saas/\">self-hosting your SaaS</a>.</p>\n<p>The goal isn't zero human involvement. It's that a bad vibe can't reach customers without something automated objecting first.</p>\n\nList every manual step between \"agent opened a PR\" and \"customers see it.\" Each one is a candidate for a pipeline check.\n\n\n- Vibe coding is a discovery tool. Use it for prototypes, UI spikes, and messy ideas, then throw the code away and keep the decision.\n- Agentic engineering is a production tool. Specs, tests, and multi-agent review are what make AI-written code safe to charge money for.\n- The skill that matters in 2026 is writing unambiguous specs. It's the new senior-engineer bottleneck, and it's a writing skill more than a coding one.\n- Give your agents memory. Vector-indexed code, docs, and incident history stop every session from restarting at zero.\n- Split your day, and enforce the boundary. The morning branch does not merge.\n- Watch the opposite failure too: over-specifying a throwaway spike is waste, not discipline.\n\n<p>If you're trying to get an AI-assisted team from prototypes to production, <a href=\"https://ansezz.com/services/\">here's how I help</a>.</p>\n<p>Which part of your workflow is still running on vibes alone, and what would an agent find if it audited that code tomorrow?</p>\n</figure></figure>",
      "date_published": "2026-07-24T00:00:00.000Z",
      "date_modified": "2026-07-24T00:00:00.000Z",
      "tags": [
        "ai",
        "vibe-coding",
        "agentic-ai",
        "ai",
        "claude",
        "laravel",
        "shopify",
        "code-quality"
      ],
      "image": "https://ansezz.com/blog/vibe-coding-vs-agentic-engineering/hero-67e56275.webp"
    },
    {
      "id": "https://ansezz.com/blog/modular-monolith-first/",
      "url": "https://ansezz.com/blog/modular-monolith-first/",
      "title": "Modular monoliths first: why microservices can wait",
      "summary": "Distributed systems are the priciest fix for a problem you don't have. How to build a modular monolith, own data per module, and know when to split.",
      "content_html": "<p>Distributed systems are the most expensive way to solve a problem you do not have yet.</p>\n\n- Start with one deployable app split into strict modules with contracts, private data and internal events.\n- Enforce module boundaries with architecture tests in CI, and never let modules share tables or JOINs.\n- Split only on real friction: team bottlenecks, divergent scaling needs or repeat fault cascades. Extract workers first.\n\n<p>Every year a new batch of SaaS founders jumps straight to microservices because they want to \"build for scale.\"</p>\n<p>Six months later they are debugging network latency and distributed transactions instead of shipping features. The product has not moved. The infrastructure bill has.</p>\n<p>The modular monolith has quietly won this argument. It gives you the logical separation you actually need without the operational tax of a distributed environment.</p>\n<p>This post covers why you should start there, how to structure it so it stays modular, and the specific signals that tell you it is finally time to split.</p>\n<p>If you are still weighing the two patterns at a high level, read <a href=\"https://ansezz.com/blog/monolith-vs-microservices/\">monolith vs microservices</a> first. This post assumes you have picked the monolith and want to build one that does not rot.</p>\n<h2>The microservice tax is real</h2>\n<p>Most startups choose microservices for the wrong reasons. They look at Netflix or Uber and assume the infrastructure is the blueprint for success.</p>\n<p>What they miss is that those companies adopted microservices to solve <strong>organizational</strong> problems (hundreds of engineers who could not coordinate a single deploy), not purely technical ones.</p>\n<p>Split prematurely and you pay a heavy tax:</p>\n<ul>\n<li><strong>Operational overhead.</strong> You now need service discovery, centralized logging, distributed tracing, and a CI/CD pipeline per repository. None of that ships a feature.</li>\n<li><strong>Data consistency.</strong> Distributed transactions are hard. You end up with eventual consistency problems that need sagas or an outbox pattern to fix: code that exists only because you split.</li>\n<li><strong>Network latency.</strong> Every cross-service call costs milliseconds and can fail. In a monolith these are function calls in memory that cannot time out.</li>\n<li><strong>Developer friction.</strong> Debugging a request that hops through five services is far worse than stepping through one process. Reproducing it locally is worse still.</li>\n</ul>\n<p>For a small team this overhead kills velocity. You spend 40% of your time on infrastructure and 60% on product. In a modular monolith that ratio flips back toward the thing customers pay for.</p>\n<figure>\n<h2>What a modular monolith actually is</h2>\n<p>A modular monolith is not a big ball of mud with nicer folder names. It is a <strong>single deployable unit</strong> where code is strictly organized into independent modules with well-defined interfaces between them.</p>\n<p>In a <a href=\"https://ansezz.com/blog/category/laravel/\">Laravel</a> codebase that means each domain gets its own namespace, its own service provider, its own routes, and its own migrations.</p>\n<p>A <code>Billing</code> module never reaches into the <code>Users</code> tables directly. It talks through a contract.</p>\n<pre><code>// app/Modules/Billing/Contracts/CustomerDirectory.php\nnamespace App\\Modules\\Billing\\Contracts;\n\ninterface CustomerDirectory\n{\n    public function find(int $customerId): ?CustomerSummary;\n}\n</code></pre>\n<pre><code>// app/Modules/Users/Adapters/EloquentCustomerDirectory.php\nnamespace App\\Modules\\Users\\Adapters;\n\nuse App\\Modules\\Billing\\Contracts\\CustomerDirectory;\nuse App\\Modules\\Billing\\Contracts\\CustomerSummary;\nuse App\\Modules\\Users\\Models\\User;\n\nfinal class EloquentCustomerDirectory implements CustomerDirectory\n{\n    public function find(int $customerId): ?CustomerSummary\n    {\n        $user = User::query()-&gt;find($customerId);\n\n        return $user\n            ? new CustomerSummary($user-&gt;id, $user-&gt;email, $user-&gt;billing_country)\n            : null;\n    }\n}\n</code></pre>\n<p><code>Billing</code> depends on an interface it owns and a DTO it controls. <code>Users</code> supplies the implementation. Swap that adapter for an HTTP client tomorrow and <code>Billing</code> does not change a single line.</p>\n<p>The seam already exists. You just move what is behind it.</p>\n<p>The goal is <strong>logical separation with physical unity</strong>. You get a clean, decoupled <a href=\"https://ansezz.com/blog/category/architecture/\">architecture</a> without managing a fleet of containers on <a href=\"https://ansezz.com/blog/coolify-docker-saas-hosting/\">Coolify</a> or Kubernetes before you need to.</p>\n<h2>Bounded contexts keep the modules honest</h2>\n<p>The concept doing the real work here is the <strong>bounded context</strong>, a term from Domain-Driven Design that marks the boundary where one model of a thing applies.</p>\n<p>A <code>Product</code> in the <code>Inventory</code> module is not the same object as a <code>Product</code> in the <code>Marketing</code> module. One cares about stock levels, SKUs, and warehouse locations. The other cares about SEO titles, hero images, and merchandising rules.</p>\n<p>Forcing them into one bloated <code>Product</code> model is how modular monoliths turn into mud.</p>\n<p>Three rules keep boundaries intact:</p>\n<ol>\n<li><strong>Enforce them mechanically.</strong> Human discipline fails under deadline. Add an architecture test that fails the build when <code>Modules\\Billing</code> imports <code>Modules\\Inventory\\Models</code>. In PHP, <a href=\"https://github.com/opensoftwareconsulting/deptrac\">Deptrac</a> or a Pest arch test does this in a few lines. Any language has an equivalent.</li>\n<li><strong>Keep the shared kernel tiny.</strong> A little shared code is fine. A giant <code>Common</code> package that every module depends on is a monolith wearing a costume; you can never split anything out of it.</li>\n<li><strong>Prefer events over direct calls.</strong> When <code>Orders</code> needs <code>Shipping</code> to react, emit a domain event instead of calling a method. Replacing an in-process listener with a message queue later becomes a config change, not a rewrite.</li>\n</ol>\n<p>That third rule pays off the most. Internal events are the cheapest dress rehearsal for <a href=\"https://ansezz.com/blog/synchronous-vs-asynchronous-communication/\">asynchronous communication</a> you will ever get, and they cost nothing while everything still runs in one process.</p>\n\nAdd one architecture test today that fails when any module imports another module's `Models` namespace. Run it and count the failures.\n\n<h2>The data ownership trap</h2>\n<p>The database is where most monolith-to-microservice migrations die.</p>\n<p>If your modules share tables or run JOINs across domains, you are coupled at the data layer, the hardest coupling to break. No amount of tidy namespaces saves you when <code>Reporting</code> joins six tables owned by four other modules.</p>\n<p>So aim for <strong>schema per module</strong>. Use one physical Postgres or MySQL instance for simplicity, but give each module its own tables and treat them as private. In practice:</p>\n<ul>\n<li>No foreign keys crossing module boundaries. Store the ID and let the application layer resolve the relationship.</li>\n<li>No cross-module JOINs. If <code>Billing</code> needs a customer email, it calls the contract, not the <code>users</code> table.</li>\n<li>Read models over reach-ins. If <code>Reporting</code> needs a wide view, have modules publish into a reporting table they explicitly own.</li>\n</ul>\n<p>Here is the test: <strong>if you cannot imagine running your <code>Orders</code> module against a separate database server tomorrow, your monolith is not modular yet.</strong> It is a monolith with folders.</p>\n\n  <div>Foreign keys across modules, `Reporting` joining six tables owned by four other modules, `Billing` reading the `users` table.</div>\n  <div>Schema per module, IDs instead of cross-module foreign keys, contracts instead of JOINs, read models each module publishes.</div>\n\n<h2>A decision framework for when to split</h2>\n<p>You rarely outgrow a monolith because of traffic. A single well-tuned Laravel box with <a href=\"https://ansezz.com/blog/laravel-octane-high-traffic/\">Octane</a> and a queue handles more load than most SaaS companies ever see.</p>\n<p>You outgrow it because of <strong>friction</strong>. Three signals matter:</p>\n<h3>1. Team autonomy</h3>\n<p>Are developers constantly blocking each other? Twenty engineers merging into one codebase with a deployment queue is a real organizational bottleneck, and that alone can justify a split.</p>\n<p>Five engineers? A monolith is almost always faster. Conway's Law cuts both ways: do not buy a distributed architecture for a co-located team.</p>\n<h3>2. Divergent scaling profiles</h3>\n<p>Does one part of the app have radically different resource needs? An AI inference module that wants GPUs while your dashboard is plain CRUD is a real case for extraction.</p>\n<p>You stop paying for GPU-sized instances to serve settings pages, and your <a href=\"https://ansezz.com/blog/category/devops/\">DevOps</a> spend tracks actual demand.</p>\n<h3>3. Fault isolation</h3>\n<p>If a bug in non-critical reporting takes down checkout, you have a resilience problem. First try to solve it in-process: better error handling, background jobs, timeouts, a bulkhead around the risky call.</p>\n<p>If it keeps happening, a separate service acts as a circuit breaker around your core revenue path.</p>\n<table>\n<thead>\n<tr>\n<th>Metric</th>\n<th>Modular monolith</th>\n<th>Microservices</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Deployment</strong></td>\n<td>Simple (one pipeline)</td>\n<td>Complex (many pipelines)</td>\n</tr>\n<tr>\n<td><strong>Testing</strong></td>\n<td>Easy (in-process)</td>\n<td>Hard (end-to-end focused)</td>\n</tr>\n<tr>\n<td><strong>Data integrity</strong></td>\n<td>Strong (ACID)</td>\n<td>Eventual consistency</td>\n</tr>\n<tr>\n<td><strong>Debugging</strong></td>\n<td>Stack trace</td>\n<td>Distributed trace</td>\n</tr>\n<tr>\n<td><strong>Operational cost</strong></td>\n<td>Low</td>\n<td>High</td>\n</tr>\n<tr>\n<td><strong>Team scaling</strong></td>\n<td>Bottlenecks past ~15 to 20</td>\n<td>Scales with squads</td>\n</tr>\n</tbody>\n</table>\n<p>Notice what is missing: raw performance. It is almost never the deciding factor, and when it is, a bigger box or a read replica usually beats a rewrite.</p>\n<h2>Extract background workers before services</h2>\n<p>When the split finally makes sense, your first move should not be a full service.</p>\n<p>Start with an <strong>extracted background worker</strong>. Move heavy, long-running tasks (PDF generation, CSV exports, video transcoding, embedding jobs) into their own process consuming from a queue.</p>\n<p>You get isolation, independent scaling, and blast-radius reduction without the pain of synchronous API contracts, retries, and versioning.</p>\n<p><a href=\"https://ansezz.com/blog/scaling-with-rabbitmq/\">RabbitMQ</a> or a <a href=\"https://ansezz.com/blog/event-driven-pubsub/\">Pub/Sub-style event backbone</a> covers most of this, and the same pattern powers <a href=\"https://ansezz.com/blog/message-queues-document-processing/\">queue-based document processing</a>.</p>\n<p>Only after that should you consider carving out a synchronous service. When you do, the <a href=\"https://ansezz.com/blog/monolith-to-microservices/\">strangler fig migration</a> is the safe path: route one endpoint at a time and keep the monolith as your fallback.</p>\n<figure>\n<h3>Wait until the domain is stable</h3>\n<p>One more constraint: <strong>wait until your domain is stable</strong>. Early-stage products change their data models weekly.</p>\n<p>Hard service boundaries during that discovery phase turn every small feature into a multi-service orchestration project. Boundaries are cheap to move inside a monolith and expensive to move across a network.</p>\n\n- **Start modular.** Separate business domains with namespaces, modules, and contracts from day one. This costs nothing early and is nearly impossible to retrofit later.\n- **Enforce boundaries with tooling.** Architecture tests in CI, not code-review vigilance.\n- **Own your data per module.** No cross-module foreign keys, no cross-domain JOINs. Data coupling is the coupling that traps you.\n- **Emit events internally.** In-process listeners today become queue consumers tomorrow with a config change.\n- **Split on friction, not fashion.** Deployment bottlenecks, divergent scaling profiles, and repeat fault cascades are reasons. A conference talk is not.\n- **Extract workers first.** Async background processes give you most of the isolation benefits at a fraction of the operational cost.\n\n<p>If your architecture lets you change your mind later without a rewrite, you have already won. That is the whole point of deferring the decision.</p>\n<p>How often are you actually checking for cross-module coupling in your codebase? If the answer is \"never,\" that is the cheapest audit you can run this week, and <a href=\"https://ansezz.com/contact/\">I am happy to look at it with you</a>.</p>\n</figure></figure>",
      "date_published": "2026-06-24T00:00:00.000Z",
      "date_modified": "2026-06-24T00:00:00.000Z",
      "tags": [
        "architecture",
        "architecture",
        "microservices",
        "laravel",
        "scaling",
        "devops"
      ],
      "image": "https://ansezz.com/blog/modular-monolith-first/hero-6a8906e7.webp"
    },
    {
      "id": "https://ansezz.com/blog/stateless-vs-stateful-apps/",
      "url": "https://ansezz.com/blog/stateless-vs-stateful-apps/",
      "title": "Stateless vs stateful apps: the architecture split",
      "summary": "Stateful servers remember; stateless servers forget by design. Why horizontal scaling, sticky sessions, and shared Redis decide the architecture split.",
      "content_html": "<p>Scaling a web application often feels like trying to upgrade a jet engine while the plane is mid-flight.</p>\n\n- Stateful servers remember users in local memory, which forces sticky sessions and breaks when you add nodes.\n- Stateless servers forget every request, so any node can serve anyone. That is what makes horizontal scaling easy.\n- Shared storage like Redis lets an app feel stateful to users while each server stays stateless.\n\n<p>You start with a single server that handles every request perfectly. But as your traffic grows, you add more servers, and suddenly everything breaks.</p>\n<p>Users are getting logged out randomly. Shopping carts are disappearing into thin air. The database is screaming under the pressure of redundant session checks.</p>\n<p>This chaos usually stems from a fundamental architectural decision made in the early stages of development. You are likely struggling with the friction between stateless and stateful design patterns.</p>\n<p>If your application relies on the server remembering who a user is based on local memory, you are building a stateful system. This works great for small projects but becomes a nightmare when you need to scale horizontally.</p>\n<p>On the other hand, moving to a fully stateless model requires a complete rethink of how you handle authentication and data persistence.</p>\n<p>You need to understand the trade-offs between stateless and stateful apps before you can build reliable, fast software in the cloud.</p>\n<h2>Defining state in the digital world</h2>\n<p>Before comparing the two architectures, we must define what \"state\" actually means in a web context.</p>\n<p>State is any data that the application needs to remember between different interactions. This could be a user's login status, the items in a Shopify cart, or the progress of a multi-step form.</p>\n<p>In a traditional web application, the server manages this state by creating a unique session for every visitor. The server stores a small file or a piece of memory linked to a specific session ID.</p>\n<p>When the user moves from one page to another, the server looks up that ID and retrieves the state. This makes the application \"stateful\" because the server's response to a request depends on what happened in previous requests.</p>\n<h2>The stateless architecture: the forgetful master</h2>\n<p>A stateless application treats every single HTTP request as a completely new event. The server does not remember anything about the client once the request is finished.</p>\n<p>A user clicks a button, the server processes the data, sends a response, and immediately forgets that user ever existed.</p>\n<figure>\n<p>For this to work, the client must provide all the necessary information with every single request. This is why stateless apps often use JSON Web Tokens (JWT) or API keys.</p>\n<p>The token contains the user's identity and permissions. When the server receives the token, it validates it, performs the requested action, and sends back the result.</p>\n<h3>Why stateless scales</h3>\n<p>The primary advantage of this approach is horizontal scalability. Since the server does not store any local session data, any server in your cluster can handle any request.</p>\n<p>You can add ten more servers to your stack using a tool like <a href=\"https://ansezz.com/blog/coolify-docker-saas-hosting/\">Coolify or Docker</a> and the load balancer can distribute traffic without worrying about where the user's session is located.</p>\n<p>This same property is what lets a stateless tier sit cleanly behind an <a href=\"https://ansezz.com/blog/api-gateway-ai-stack/\">API gateway</a>.</p>\n<h2>The stateful architecture: the memory expert</h2>\n<p>Stateful applications are built on the premise of continuity. The server maintains a persistent connection or a session record for every active user.</p>\n<p>This was the default way of building the web for decades. If you are using Laravel with standard Blade templates and the built-in authentication guard, you are likely running a stateful application.</p>\n<p>In a stateful system, the server's internal memory or local disk stores the user's context. This makes development faster and easier for certain features.</p>\n<p>For example, \"flashing\" a success message to the next page is trivial in a stateful app. The server just puts the message in the session and reads it on the next load.</p>\n<h3>The sticky session problem</h3>\n<p>However, statefulness introduces a \"sticky session\" problem. If you have three servers (A, B, and C) and a user's session is stored on Server A, the load balancer must ensure that every subsequent request from that user goes back to Server A.</p>\n<p>If the user hits Server B, that server will have no record of them, and they will appear logged out.</p>\n<p>Sticky sessions make scaling significantly more complex and create a single point of failure.</p>\n<p>If Server A crashes, all users \"stuck\" to it lose their session data.</p>\n<figure>\n<h2>Stateless vs stateful apps: key differences at a glance</h2>\n<p>Choosing between these two paths depends on your specific technical requirements. Below is a breakdown of how they compare across critical engineering metrics.</p>\n<table>\n<thead>\n<tr>\n<th>Feature</th>\n<th>Stateless Apps</th>\n<th>Stateful Apps</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Data storage</strong></td>\n<td>State is stored on the client or a shared database/cache.</td>\n<td>State is stored in the server's memory or local storage.</td>\n</tr>\n<tr>\n<td><strong>Scalability</strong></td>\n<td>High. Any server can process any request.</td>\n<td>Low. Requires sticky sessions or complex synchronization.</td>\n</tr>\n<tr>\n<td><strong>Resilience</strong></td>\n<td>High. If a server dies, another one takes over instantly.</td>\n<td>Moderate. Server failure often leads to session loss.</td>\n</tr>\n<tr>\n<td><strong>Complexity</strong></td>\n<td>Higher initial setup for token management and external caching.</td>\n<td>Lower initial setup for small-scale projects.</td>\n</tr>\n<tr>\n<td><strong>Performance</strong></td>\n<td>Potential overhead from passing tokens or fetching data from Redis.</td>\n<td>Faster local memory access but slower overall scaling.</td>\n</tr>\n</tbody>\n</table>\n<h2>Practical implementation in Laravel</h2>\n<p>In the world of PHP development, Laravel gives you the flexibility to choose either path. By default, the <code>web</code> middleware group uses stateful sessions.</p>\n<p>It stores a session cookie on the browser and matches it to a server-side entry, which since Laravel 11 lands in the <code>database</code> driver out of the box. This is perfect for traditional dashboards where you need simple state management and cookie-based CSRF protection.</p>\n<p>To make your Laravel app stateless, you move toward token-based authentication using Laravel Sanctum or Passport.</p>\n<p>You skip session cookies on your API routes and let every request carry its own bearer token, validating the user against a shared store like Redis or the database. This shift is essential when building backends for mobile apps or complex React/Vue frontends.</p>\n<p>This is also where the stateless model pays off as you grow. Because no node owns a session, you can lean on <a href=\"https://ansezz.com/blog/horizontal-vs-vertical-scaling/\">horizontal scaling</a> and add capacity behind the load balancer without any session-affinity gymnastics.</p>\n\nOpen your production `.env` and check `SESSION_DRIVER`. If it says `file` and you run more than one node, that is your random-logout bug.\n\n<h2>Scaling with Shopify and modern e-commerce</h2>\n<p>Shopify provides an interesting look at how these architectures live together. The Shopify platform itself is a massive distributed system.</p>\n<p>While the \"state\" of a store (orders, inventory, customers) is preserved in giant databases, the web tier that handles incoming traffic is designed to be stateless. This allows Shopify to handle massive spikes during Black Friday by spinning up thousands of web workers that don't need to share local memory.</p>\n<p>If you are building <a href=\"https://ansezz.com/blog/agentic-commerce-shopify/\">agentic commerce solutions on Shopify</a>, your app architecture should lean toward statelessness. Your app will receive webhooks and API calls from various Shopify nodes.</p>\n<p>If your app is stateful and relies on local memory to track a multi-step checkout process, you will likely encounter race conditions or lost data as Shopify's infrastructure scales.</p>\n<p>Using a shared Redis instance to store temporary job state is the professional way to handle this.</p>\n<h2>DevOps and deployment considerations</h2>\n<p>Your choice between stateless vs stateful apps will dictate your DevOps strategy. Stateless apps are the \"gold standard\" for containerization.</p>\n<p>Since they have no local dependencies, you can run them in Docker containers and deploy them to any cloud provider without friction. You don't need to worry about preserving the \"health\" of a specific server instance.</p>\n<h3>Stateful apps need more care</h3>\n<p>Stateful apps require much more care. If you are deploying a stateful Laravel app, you must ensure that your session driver is set to something central like Redis or a database.</p>\n<p>Laravel 11 and later default to the <code>database</code> driver, but if you switch it back to <code>file</code>, each node writes sessions to its own disk and your horizontal scaling will fail.</p>\n<p>You also need to consider how you handle deployments. A \"zero-downtime\" deployment is harder when you have thousands of active sessions living in server memory. You often have to wait for sessions to drain before taking a node offline.</p>\n\nBefore your next deploy, take one node out of the load balancer and watch whether any users get logged out. That tells you how stateful you really are.\n\n\n- **Stateless apps** treat every request as a new event, requiring the client to provide context (tokens) every time.\n- **Stateful apps** remember user interactions through local server memory or session files, making them easier to build but harder to scale.\n- **Horizontal scaling** is the primary driver for choosing a stateless architecture. It allows you to add servers without session conflicts.\n- **Laravel** supports both. Use sessions for simple web apps and tokens (Sanctum) for scalable APIs.\n- **Shared storage** (like Redis) is the bridge that allows an application to feel stateful to the user while remaining stateless at the server level.\n- **Shopify developers** should prioritize stateless designs to handle the platform's distributed nature and high-volume webhooks.\n\n<p>How are you managing session persistence in your current production environment to ensure horizontal scaling?</p>\n</figure></figure>",
      "date_published": "2026-06-23T00:00:00.000Z",
      "date_modified": "2026-06-23T00:00:00.000Z",
      "tags": [
        "architecture",
        "architecture",
        "scaling",
        "laravel",
        "redis",
        "devops",
        "shopify"
      ],
      "image": "https://ansezz.com/blog/stateless-vs-stateful-apps/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/docker-vs-kubernetes/",
      "url": "https://ansezz.com/blog/docker-vs-kubernetes/",
      "title": "Docker vs Kubernetes: containers vs orchestration",
      "summary": "Docker packages your app into a portable box; Kubernetes manages a fleet of them across a cluster. How they differ and when you need orchestration.",
      "content_html": "<p>Shipping software is hard.</p>\n\n- Docker packages your app into a portable image and runs containers on one host.\n- Kubernetes orchestrates many containers across a cluster: scheduling, scaling, self-healing and rolling deploys.\n- Small teams can stay on Docker or Coolify. Reach for Kubernetes when availability and scale demand it.\n\n<p>You build an app that works perfectly on your machine, but the moment it hits production, everything breaks.</p>\n<p>This \"it works on my machine\" syndrome is exactly why understanding the difference between Docker vs Kubernetes is essential for modern engineers.</p>\n<p>Environment inconsistencies create friction, delay releases, and lead to late-night debugging sessions that could have been avoided with the right tooling.</p>\n<p>The gap between local development and production scale is wide. One tool packages your application into a portable box; the other manages a fleet of those boxes across a cluster of servers.</p>\n<p>If you are shipping modern web or e-commerce applications, you eventually need both, and knowing where each one stops saves you a lot of wasted effort.</p>\n<h2>The container revolution with Docker</h2>\n<p>Docker changed how we think about software delivery by introducing the concept of containerization.</p>\n<p>A container is a lightweight, standalone package that includes everything needed to run an application. This includes the code, runtime, system tools, libraries, and settings.</p>\n<p>When you use Docker, you are essentially creating an immutable image of your software. This image behaves exactly the same whether it is running on your laptop, a colleague's machine, or a staging server.</p>\n<p>It eliminates the need to manually configure environments, which is a major win for developer productivity.</p>\n<h3>One host at a time</h3>\n<p>Docker operates on a single-host level. It manages the lifecycle of individual containers. You use the Docker CLI to build, run, and stop these containers.</p>\n<p>For many small-scale projects or early-stage startups, Docker on a single VPS is often enough. In fact, tools like <a href=\"https://ansezz.com/blog/coolify-docker-saas-hosting/\">Coolify for Docker SaaS hosting</a> have made it easy to manage these deployments without needing a massive orchestration layer.</p>\n<figure>\n<h2>Understanding Kubernetes orchestration</h2>\n<p>If Docker is about the container, Kubernetes is about the cluster. Kubernetes, often abbreviated as K8s, is an open-source platform designed to automate the deployment, scaling, and management of containerized applications.</p>\n<p>It does not compete with Docker. Docker builds the image; Kubernetes schedules and runs it at scale through whatever container runtime sits on each node.</p>\n<p>Imagine you have a high-traffic Shopify app that suddenly experiences a surge in users. Managing fifty individual Docker containers across five different servers manually would be a nightmare.</p>\n<p>You would have to track which server has space, handle load balancing, and manually restart any container that crashes.</p>\n<h3>Desired state</h3>\n<p>Kubernetes handles this automatically. It uses a declarative approach where you define the \"desired state\" of your system in YAML files.</p>\n<p>The Kubernetes control plane then keeps the actual state matching what you declared. If a container dies, Kubernetes restarts it. If a node fails, it reschedules the work elsewhere.</p>\n<figure>\n<h2>Docker vs Kubernetes: key technical differences</h2>\n<p>Comparing Docker vs Kubernetes is not about choosing one over the other. They operate at different layers of the stack.</p>\n<p>Docker is the runtime that builds and runs the container. Kubernetes is the orchestration layer that schedules those containers across a network.</p>\n<table>\n<thead>\n<tr>\n<th>Feature</th>\n<th>Docker</th>\n<th>Kubernetes</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Primary goal</strong></td>\n<td>Containerizing an application</td>\n<td>Managing a cluster of containers</td>\n</tr>\n<tr>\n<td><strong>Scaling</strong></td>\n<td>Manual scaling on a single host</td>\n<td>Automated horizontal scaling</td>\n</tr>\n<tr>\n<td><strong>Self-healing</strong></td>\n<td>Restart policies, single host</td>\n<td>Probe-based restarts and rescheduling</td>\n</tr>\n<tr>\n<td><strong>Networking</strong></td>\n<td>Basic host-level networking</td>\n<td>Advanced cluster-wide networking</td>\n</tr>\n<tr>\n<td><strong>Complexity</strong></td>\n<td>Low to medium</td>\n<td>High</td>\n</tr>\n</tbody>\n</table>\n<p>Docker focuses on the \"how\" of running an app. Kubernetes focuses on the \"where\" and \"how many\" of running those apps across a distributed system.</p>\n<p>If the unit of scheduling is new to you, it helps to understand the <a href=\"https://ansezz.com/blog/container-vs-pod/\">difference between a container and a pod</a> before you reach for orchestration.</p>\n<h2>How they work together in production</h2>\n<p>In a professional DevOps workflow, Docker and Kubernetes are teammates. The process usually starts with a Dockerfile.</p>\n<p>You build a Docker image and push it to a private container registry. This image is your \"source of truth\" for the application.</p>\n<p>Next, you create Kubernetes manifests. These files tell the cluster which image to use, how many replicas are needed, and what environment variables to inject.</p>\n<p>When you apply these manifests, the kubelet on each node calls the container runtime to pull the image and start the container.</p>\n<h3>The dockershim caveat</h3>\n<p>One caveat that trips people up: since version 1.24, Kubernetes removed dockershim, so it no longer talks to Docker Engine directly. Nodes now run a CRI-compliant runtime such as containerd or CRI-O.</p>\n<p>The image you built with Docker still runs fine because it follows the OCI image spec, but Docker Engine itself is no longer the thing executing it.</p>\n<h3>Rolling updates</h3>\n<p>Together, the two make updates smooth. You can perform rolling deployments where Kubernetes replaces old pods with new ones in a controlled rollout. This keeps the service available throughout.</p>\n<p>If the new version has a bug, Kubernetes stops the rollout when health checks fail, and one command (<code>kubectl rollout undo</code>) takes you back to the previous stable image.</p>\n\nRun `kubectl get nodes -o wide` on your cluster and look at the CONTAINER-RUNTIME column. It will most likely say containerd, not Docker.\n\n<h2>Choosing the right path for your project</h2>\n<p>Not every project needs the overhead of Kubernetes. If you are an entrepreneur or a small team building a simple web application, starting with Docker Compose or a <a href=\"https://ansezz.com/blog/coolify-self-hosted-saas/\">self-hosted SaaS platform</a> might be more efficient.</p>\n<p>It allows you to ship fast without managing a complex cluster.</p>\n<p>However, if your business requires high availability, handles massive amounts of data, or uses a microservices architecture, Kubernetes is the industry standard.</p>\n<p>It provides the <a href=\"https://ansezz.com/blog/horizontal-vs-vertical-scaling/\">horizontal scaling</a> needed to grow from a few hundred users to millions without changing your core infrastructure.</p>\n<h3>Shopify at scale</h3>\n<p>Shopify itself is a good example of the payoff: its platform runs on Kubernetes across hundreds of clusters on Google Kubernetes Engine, autoscaling stateless workloads to absorb the brutal traffic spikes of Black Friday and Cyber Monday.</p>\n<p>Most merchants never touch that layer because Shopify owns it. But if you are building custom apps or your own infrastructure at that scale, the ability to declare capacity and let the cluster reconcile it is the competitive advantage that outweighs the setup complexity.</p>\n\n  <div>Small team, simple web app, ship fast with Docker Compose or Coolify. You restart and scale by hand.</div>\n  <div>High availability, big data, microservices. The cluster schedules, scales and heals from the desired state you declare.</div>\n\n\nWrite down how many servers and containers you run today and how often one fails. If the answer is \"one server, rarely,\" Kubernetes can wait.\n\n\n- **Docker** is for creating, packaging, and running individual containers on a host.\n- **Kubernetes** is for orchestrating and managing fleets of containers across a cluster of servers.\n- **Synergy**: You use Docker to build the images and Kubernetes to deploy and scale them.\n- **Scale matters**: Small projects should stick to Docker to avoid unnecessary complexity. Enterprise-grade apps require Kubernetes for self-healing and autoscaling.\n- **Declarative power**: Kubernetes uses YAML to maintain a desired state, making infrastructure management more predictable.\n\n<p>If you're weighing that jump right now, <a href=\"https://ansezz.com/services/\">here's how I help teams get the call right</a>.</p>\n<p>At what stage of growth did your infrastructure move from a single server to a distributed cluster?</p>\n</figure></figure>",
      "date_published": "2026-06-22T00:00:00.000Z",
      "date_modified": "2026-06-22T00:00:00.000Z",
      "tags": [
        "devops",
        "docker",
        "kubernetes",
        "devops",
        "scaling"
      ],
      "image": "https://ansezz.com/blog/docker-vs-kubernetes/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/forward-proxy-vs-reverse-proxy/",
      "url": "https://ansezz.com/blog/forward-proxy-vs-reverse-proxy/",
      "title": "Forward proxy vs reverse proxy: the technical guide",
      "summary": "A forward proxy hides the client; a reverse proxy hides the server. How traffic direction shapes your load balancing, SSL termination, and security design.",
      "content_html": "<p>Most developers interact with proxies every single day without realizing which direction the traffic is actually flowing.</p>\n\n- A forward proxy speaks for the client and hides its IP. It handles outbound traffic, filtering and anonymity.\n- A reverse proxy speaks for the server and hides the backend. It handles load balancing, SSL termination, WAF and caching.\n- In Docker and Laravel setups, Nginx as a reverse proxy with the right forwarded headers is the pattern you will touch most.\n\n<p>The choice between a forward proxy vs reverse proxy comes down to one question: whose side is the middleman on?</p>\n<p>You set up Nginx for a Laravel app, and it works. You route outbound traffic through a corporate proxy, and it works.</p>\n<p>But when the site goes down under heavy load or your IP gets blacklisted, that distinction becomes the difference between a quick fix and hours of infrastructure debt.</p>\n<p>In modern web architecture, a proxy is essentially a \"middleman\" server. It sits between two parties to facilitate communication.</p>\n<p>However, the side it represents determines its name, its function, and its security benefits. This guide breaks down the core technical differences to help you choose the right pattern for your next deployment.</p>\n<h2>The forward proxy: protecting the client</h2>\n<p>A forward proxy sits in front of one or more client machines and acts as their representative to the open internet.</p>\n<p>When a user makes a request to a website, the request first hits the forward proxy. The proxy then evaluates the request, sends it to the web server, and passes the response back to the user.</p>\n<p>From the perspective of the internet, the forward proxy is the one making the request. The original client's IP address remains hidden behind the proxy's IP. This is a common setup in corporate environments or for users seeking anonymity.</p>\n<figure>\n<h3>Why use a forward proxy?</h3>\n<p>Forward proxies are primarily used to manage and secure outbound traffic. Here are the most common engineering use cases:</p>\n<ul>\n<li><strong>Anonymity and privacy</strong>: By masking the client IP, forward proxies prevent websites from tracking individual users based on their network location.</li>\n<li><strong>Content filtering</strong>: Organizations use forward proxies to block access to specific URLs or categories of websites. If a request violates a security policy, the proxy simply drops it.</li>\n<li><strong>Bypassing geo-restrictions</strong>: If you need to access a service only available in another region, a forward proxy located in that region can make your request appear local.</li>\n<li><strong>Caching for performance</strong>: If multiple clients in the same network request the same external resource, the forward proxy can cache that response locally. This saves bandwidth and reduces latency for subsequent requests.</li>\n</ul>\n<h2>The reverse proxy: protecting the server</h2>\n<p>While a forward proxy represents the client, a reverse proxy represents the server. It sits at the edge of your infrastructure and intercepts all incoming requests from the internet before they reach your backend application.</p>\n<p>The client believes they are communicating directly with the origin server. In reality, they are talking to the reverse proxy. The proxy then decides which backend server should handle the request.</p>\n<p>This architecture is a cornerstone of <a href=\"https://ansezz.com/blog/category/devops/\">modern cloud infrastructure</a> and high-availability systems.</p>\n<figure>\n<h3>Why use a reverse proxy?</h3>\n<p>Reverse proxies are essential for scaling and securing web applications. They handle several \"heavy lifting\" tasks that your application code shouldn't have to manage.</p>\n<ul>\n<li><strong>Load balancing</strong>: This is the most common use case. A reverse proxy like Nginx or HAProxy distributes incoming traffic across multiple app nodes so no single server becomes a bottleneck. The overlap here is real enough that it's worth reading <a href=\"https://ansezz.com/blog/load-balancer-vs-reverse-proxy/\">load balancer vs reverse proxy</a> to see where the two roles diverge.</li>\n<li><strong>SSL termination</strong>: Managing TLS certificates on every backend server is a nightmare. A reverse proxy decrypts incoming HTTPS at the edge and forwards plain HTTP across a trusted internal network. If compliance demands end-to-end encryption, you re-encrypt to the backend with <code>proxy_pass https://</code> and <code>proxy_ssl_verify on</code> instead.</li>\n<li><strong>Security and WAF</strong>: Reverse proxies hide the existence and characteristics of your backend servers. You can run a Web Application Firewall (WAF) at the proxy level to filter SQL injection and XSS before they ever hit your code, and absorb volumetric DDoS traffic at the edge.</li>\n<li><strong>Caching</strong>: By caching static assets or even entire HTML responses, a reverse proxy serves requests without touching the database or the application server. Push that cache out to the network edge and you are effectively running a <a href=\"https://ansezz.com/blog/cdn-vs-cache/\">CDN, which is why high-traffic sites need both</a>.</li>\n</ul>\n<h2>Forward proxy vs reverse proxy: visibility and flow</h2>\n<p>The easiest way to remember the difference is to look at who is being \"hidden\" from whom.</p>\n<table>\n<thead>\n<tr>\n<th>Feature</th>\n<th>Forward Proxy</th>\n<th>Reverse Proxy</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Who it protects</strong></td>\n<td>The client</td>\n<td>The server</td>\n</tr>\n<tr>\n<td><strong>IP hiding</strong></td>\n<td>Hides client IP from server</td>\n<td>Hides server IP from client</td>\n</tr>\n<tr>\n<td><strong>Placement</strong></td>\n<td>Internal network (client-side)</td>\n<td>Edge of the network (server-side)</td>\n</tr>\n<tr>\n<td><strong>Primary goal</strong></td>\n<td>Privacy, filtering, egress control</td>\n<td>Load balancing, security, performance</td>\n</tr>\n<tr>\n<td><strong>Example tool</strong></td>\n<td>Squid, Privoxy</td>\n<td>Nginx, HAProxy, Cloudflare</td>\n</tr>\n</tbody>\n</table>\n<p>In a forward proxy setup, the origin server has no idea who the actual client is. In a reverse proxy setup, the client has no idea which backend server actually processed their request.</p>\n<p>Both are essential for <a href=\"https://ansezz.com/blog/category/architecture/\">clean architecture</a> but solve entirely different problems.</p>\n<h2>Engineering implementations: Nginx and Docker</h2>\n<p>When building modern web applications, you are most likely to interact with reverse proxies. If you are using a tool like <a href=\"https://ansezz.com/blog/coolify-docker-saas-hosting/\">Coolify or Docker</a>, the reverse proxy is often the component that manages your containers.</p>\n<p>For instance, an Nginx reverse proxy configuration for a Laravel application might look like this:</p>\n<pre><code>server {\n    listen 80;\n    server_name myapp.com;\n\n    location / {\n        proxy_pass http://app_container:8000;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Proto $scheme;\n    }\n}\n</code></pre>\n<p>In this setup, Nginx acts as the reverse proxy. It receives the request from the user and forwards it to the <code>app_container</code>.</p>\n<p>It also passes along the <code>X-Forwarded-For</code> header so your application can still know the original user's IP address if needed.</p>\n<p>This is a critical pattern when building an <a href=\"https://ansezz.com/blog/api-gateway-ai-stack/\">API gateway for an AI stack</a> or managing Shopify app extensions.</p>\n\nLog `$request-&gt;ip()` in your Laravel app behind the proxy. If it shows the proxy's address instead of the user's, your forwarded headers or trusted proxies need fixing.\n\n<h2>Advanced use cases: the mesh approach</h2>\n<p>In complex microservices environments, the lines blur. Service meshes like Istio and Linkerd typically deploy a \"sidecar\" proxy next to every service.</p>\n<p>Each sidecar acts as both a forward and a reverse proxy depending on whether the traffic is ingress (incoming) or egress (outgoing).</p>\n<p>This gives you fine-grained control over how services talk to each other. You can implement automatic retries, circuit breaking, and mutual TLS without writing a single line of application code.</p>\n<p>While this adds complexity, it provides the level of observability and reliability required for enterprise-grade systems.</p>\n\n- **Forward proxies protect the user.** Use them when you want to control how your internal network accesses the internet or to maintain user anonymity.\n- **Reverse proxies protect the infrastructure.** Use them to scale your applications through load balancing, improve performance via caching, and secure your backend.\n- **Direction matters.** A forward proxy handles outbound requests. A reverse proxy handles inbound requests.\n- **Nginx is the industry standard.** It is the most common tool for implementing reverse proxies in [Laravel and Docker environments](/blog/category/laravel/).\n- **Security is centralized.** Both types of proxies allow you to centralize security policies rather than implementing them on every individual device or server.\n\n<p>Understanding which direction traffic flows is the first step toward building resilient systems.</p>\n<p>If you are managing a growing web application, you should focus on your reverse proxy configuration. It is one of the most useful tools you have for uptime and performance.</p>\n<p>Have you ever found yourself troubleshooting an \"IP mismatch\" error caused by a misconfigured proxy header?</p>\n</figure></figure>",
      "date_published": "2026-06-22T00:00:00.000Z",
      "date_modified": "2026-06-22T00:00:00.000Z",
      "tags": [
        "devops",
        "networking",
        "devops",
        "infrastructure",
        "security",
        "docker"
      ],
      "image": "https://ansezz.com/blog/forward-proxy-vs-reverse-proxy/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/dns-vs-service-discovery/",
      "url": "https://ansezz.com/blog/dns-vs-service-discovery/",
      "title": "DNS vs service discovery",
      "summary": "DNS was built for servers that live for years; microservices live for minutes. Why service discovery beats stale DNS records for high-churn systems.",
      "content_html": "<p>DNS was built for servers that live for years. Your containers live for minutes.</p>\n\n- DNS maps a name to an IP and leans on caching, so changes are slow to reach clients and it has no idea whether an instance is healthy.\n- Service discovery keeps a live catalog of running instances, runs health checks and pushes changes to clients almost instantly.\n- The best setup for most teams is hybrid: a dynamic registry (Kubernetes or Consul) that still answers plain DNS queries.\n\n<p>You deploy a new version of your microservice and the old containers are killed instantly. You update your internal DNS records to point to the new IP addresses.</p>\n<p>However, for the next five minutes, your API gateway continues to send 40 percent of your traffic to the dead containers. Your logs fill with connection timeouts and your error rate spikes.</p>\n<p>This is the classic DNS staleness trap that plagues modern distributed systems.</p>\n<p>The problem lies in how we locate resources. In a modern cloud environment, servers might only live for minutes. Relying on static naming for high-churn infrastructure creates a gap between where your traffic goes and where your healthy services actually live.</p>\n<p>This article explores the technical differences between <strong>DNS vs service discovery</strong>, why the distinction matters for your uptime, and how to choose the right strategy for your <a href=\"https://ansezz.com/blog/category/devops/\">DevOps architecture</a>.</p>\n<h2>DNS vs service discovery: the map and the radar</h2>\n<p>At its core, DNS is a distributed, hierarchical naming system. It acts like the Yellow Pages for the internet. You give it a hostname, and it returns an IP address.</p>\n<p>It is very efficient and available everywhere. Every programming language and operating system knows how to talk to it.</p>\n<p>Service discovery is a more specialized control plane. Instead of a static map, it acts like a live radar. It tracks the current state of every service instance in your cluster.</p>\n<p>DNS tells you where a service should be. Service discovery tells you where it is right now.</p>\n<h3>The service catalog</h3>\n<p>A service discovery system like Consul, or the internal registry in Kubernetes, maintains a \"service catalog.\" This catalog is a live database of every running instance.</p>\n<p>When a new container starts, it registers itself with the catalog. When it shuts down, it is removed. This happens in milliseconds, far faster than traditional DNS propagation.</p>\n<figure>\n\nList the internal hostnames your services call each other by. For each one, write down what updates the record when an instance dies. If the answer is \"a person,\" you have a staleness risk.\n\n<h2>The TTL problem and DNS caching</h2>\n<p>The biggest hurdle in using DNS for microservices is Time-To-Live (TTL). DNS relies on caching so that every request does not hammer the name servers.</p>\n<p>Your operating system caches the result. Your browser caches the result. Your <a href=\"https://ansezz.com/blog/api-gateway-ai-stack/\">API gateway</a> caches the result.</p>\n<h3>Low TTLs are not a fix</h3>\n<p>If you set a high TTL, your service changes are slow to propagate. If you set a very low TTL (like 0 or 1 second), you might overwhelm your DNS server with lookups.</p>\n<p>Even worse, some runtimes ignore the TTL entirely. The JVM, for example, caches a successful lookup for 30 seconds by default and forever when a security manager is set, so it can keep a stale IP until the process restarts.</p>\n<table>\n<thead>\n<tr>\n<th>Feature</th>\n<th>DNS</th>\n<th>Service Discovery</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Primary Goal</strong></td>\n<td>Human-readable name to IP</td>\n<td>Dynamic instance tracking</td>\n</tr>\n<tr>\n<td><strong>Propagation</strong></td>\n<td>Slow (TTL dependent)</td>\n<td>Near-instant (Gossip/Streaming)</td>\n</tr>\n<tr>\n<td><strong>Metadata</strong></td>\n<td>Very limited (TXT records)</td>\n<td>Rich (tags, version, region)</td>\n</tr>\n<tr>\n<td><strong>Client Support</strong></td>\n<td>Native in all OS</td>\n<td>Often requires API/Agent</td>\n</tr>\n<tr>\n<td><strong>Health Awareness</strong></td>\n<td>None (Static)</td>\n<td>Active monitoring</td>\n</tr>\n</tbody>\n</table>\n<h3>Push instead of wait</h3>\n<p>Service discovery solves this by moving away from deep caching. Clients or load balancers often keep a streaming connection to the registry.</p>\n<p>When a service instance moves, the registry pushes a notification to all subscribers. There is no waiting for a cache to expire. The system converges on the new state almost instantly.</p>\n\nCheck the DNS cache settings of your runtime. On the JVM, look at `networkaddress.cache.ttl` in the `java.security` file and set it to a short value if your services resolve internal names.\n\n<h2>Health awareness: the core difference</h2>\n<p>A standard DNS server does not know whether the IP addresses it returns are healthy. If a server rack loses power, the DNS record still points to those IPs.</p>\n<p>You have to update the record by hand or run a custom script to change it.</p>\n\n  <div>An instance dies. The record still points at it until someone or some script changes it, and caches keep the old answer even longer.</div>\n  <div>An instance fails its health check. The registry marks it unhealthy and stops returning it to clients.</div>\n\n<h3>Health checks as a first-class feature</h3>\n<p>Service discovery systems run active health checks on every registered instance. A check can be as simple as a TCP probe or as complex as a custom HTTP endpoint that checks database connectivity.</p>\n<p>If a health check fails, the instance is marked \"unhealthy\" in the registry. The discovery system then stops returning that IP address to clients.</p>\n<p>This gives you a level of self-healing that plain DNS cannot, and it pairs naturally with the signals you already collect through <a href=\"https://ansezz.com/blog/logging-vs-monitoring/\">logging and monitoring</a>.</p>\n<p>If you are <a href=\"https://ansezz.com/blog/coolify-docker-saas-hosting/\">hosting a SaaS on Docker</a>, automated health-aware routing is the difference between a minor blip and a major outage.</p>\n<figure>\n\nAdd a `/health` endpoint to one service that checks its own dependencies (database, cache), not just \"the process is up.\" Point your registry or load balancer at it.\n\n<h2>Metadata and rich discovery</h2>\n<p>DNS is built to return one type of data: an IP address. You can use SRV records for port numbers or TXT records for strings, but the interface is clunky. It does not support complex queries.</p>\n<p>Service discovery allows metadata-driven routing. You can ask the registry for \"all healthy instances of the 'orders' service running version 2.1 in the 'us-east-1' region.\"</p>\n<h3>Deployment patterns it enables</h3>\n<ol>\n<li><strong>Canary Deployments</strong>: Routing only 5 percent of traffic to a specific version tag.</li>\n<li><strong>Blue/Green Deployments</strong>: Swapping traffic between service sets by changing a metadata flag.</li>\n<li><strong>Locality-Aware Routing</strong>: Directing traffic to the instance physically closest to the requester to reduce latency.</li>\n</ol>\n<p>This level of detail is essential for scaling complex systems where simple round-robin load balancing is not enough.</p>\n<h2>How modern orchestrators bridge the gap</h2>\n<p>In <a href=\"https://ansezz.com/blog/docker-vs-kubernetes/\">Kubernetes</a>, you still use DNS names to talk to services. You might call <code>http://order-service.default.svc.cluster.local</code>. Does this mean Kubernetes just uses DNS?</p>\n<p>Not exactly. Kubernetes uses a hybrid approach. It runs CoreDNS, but CoreDNS is not backed by static zone files. It is plugged directly into the Kubernetes API.</p>\n<h3>CoreDNS watches the API</h3>\n<p>When a Pod starts or dies, the control plane updates the Service and EndpointSlice objects. CoreDNS watches those objects through the API and updates its answers in real time.</p>\n<p>For a normal ClusterIP Service, the name resolves to one stable virtual IP, and kube-proxy updates the routing behind it as Pods come and go. For a headless Service, the name resolves straight to the current Pod IPs.</p>\n<p>In this model, DNS is just the \"wire protocol.\" It is the interface your application uses because it is easy and standard. The backend is a fully dynamic service discovery engine.</p>\n<p>That gives you the simplicity of DNS and the speed of service discovery.</p>\n\nRun `kubectl get endpointslices -l kubernetes.io/service-name=` while you scale a Deployment up and down. Watch the addresses change without touching any DNS record.\n\n<h2>Implementation strategies: when to use which?</h2>\n<p>Choosing between <strong>DNS vs service discovery</strong> depends on your infrastructure scale and churn rate.</p>\n<p>If you run a monolithic application on a few virtual machines that rarely change, DNS is perfectly fine. You can manage your records with simple automation or even by hand. A dedicated discovery system would cost more than it gives back.</p>\n<p>If you use containers, serverless functions, or microservices, you need a dynamic solution. If your IPs change every time you deploy or scale, DNS will eventually break your traffic flow.</p>\n<h3>A DNS interface on a dynamic registry</h3>\n<p>For many developers, the best path is a tool that provides a DNS interface on top of a dynamic registry. HashiCorp Consul is a popular choice for this.</p>\n<p>It lets you query the registry with a standard DNS lookup, while it handles the health checking and registration logic behind the scenes.</p>\n\nCount how often your instance IPs changed last week. A few times a month means DNS is fine. Several times a day means you want a registry.\n\n\n1.  **DNS is for stability**: Use it for external traffic, long-lived resources, and cross-company communication where caching is an advantage.\n2.  **Service Discovery is for churn**: Use it for internal microservices, containers, and autoscaling groups where instances appear and disappear frequently.\n3.  **Mind the Cache**: Never rely on plain DNS for sub-minute failover because client-side caching will almost always outlive your records.\n4.  **Health is Binary**: Without active health checks, your discovery system is just a list of guesses. Always integrate automated liveness and readiness probes.\n5.  **Hybrid is Best**: Use a discovery engine (like Kubernetes or Consul) that exposes a DNS interface. This simplifies your application code while keeping your infrastructure dynamic.\n\n<p>If you're untangling service routing for a high-churn stack, <a href=\"https://ansezz.com/services/\">here's how I help teams ship it</a>.</p>\n<p>When was the last time a stale DNS record caused a production incident in your environment?</p>\n</figure></figure>",
      "date_published": "2026-06-21T00:00:00.000Z",
      "date_modified": "2026-06-21T00:00:00.000Z",
      "tags": [
        "devops",
        "networking",
        "kubernetes",
        "microservices",
        "devops",
        "infrastructure"
      ],
      "image": "https://ansezz.com/blog/dns-vs-service-discovery/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/bandwidth-vs-throughput/",
      "url": "https://ansezz.com/blog/bandwidth-vs-throughput/",
      "title": "Bandwidth vs throughput: a wider pipe won't fix lag",
      "summary": "Bandwidth is capacity, throughput is delivery. Why a wider pipe never fixes a throughput bottleneck, and the latency and protocol levers that do.",
      "content_html": "<p>You pay for a 1 Gbps fiber line, yet your API calls take 500ms and your backups crawl. You have a throughput problem, not a bandwidth problem.</p>\n\n- Bandwidth is the capacity of the link. Throughput is the data that actually arrives. Throughput is almost always lower.\n- Latency caps throughput: every round trip your app waits on is time the pipe sits idle, no matter how wide it is.\n- Smaller payloads, fewer round trips, reused connections and modern protocols raise throughput more than a bigger plan does.\n\n<p>Most engineering teams mistake capacity for performance. They assume that widening the pipe automatically solves the lag.</p>\n<p>This misunderstanding leads to over-provisioned cloud bills and under-performing applications. You are throwing money at a bandwidth problem when you actually have a throughput bottleneck.</p>\n<p>Bandwidth vs throughput is the difference between a theoretical promise and a practical delivery. In high-performance web development and Shopify ecosystems, knowing which one limits you is what lets you scale without overpaying.</p>\n<h2>The capacity promise of bandwidth</h2>\n<p>Bandwidth is the maximum theoretical amount of data that can pass through a network path in a given time. Think of it as the number of lanes on a highway.</p>\n<p>A twelve-lane highway has massive bandwidth. You have the potential to move thousands of cars at once.</p>\n<p>In technical terms, bandwidth is a measure of capacity. It is usually expressed in bits per second (bps), Megabits per second (Mbps), or Gigabits per second (Gbps).</p>\n<p>When a cloud provider like Google Cloud or AWS quotes you a network speed, they are selling you bandwidth. It is a hard ceiling on how much data can move.</p>\n<h3>Bandwidth is passive</h3>\n<p>Bandwidth does not account for the speed of the individual cars or a traffic jam at the exit ramp.</p>\n<p>You can have a 100 Gbps connection, but if the protocol or the destination server cannot keep up, most of that bandwidth stays unused.</p>\n<h2>The reality check of throughput</h2>\n<p>Throughput is the actual amount of data that successfully travels from point A to point B in a specific timeframe. If bandwidth is the number of lanes, throughput is the number of cars that arrive every minute.</p>\n\n  <div>How many lanes the highway has. The most data the link could carry.</div>\n  <div>How many cars actually arrive each minute. The data that really got delivered.</div>\n\n<h3>Why throughput is lower</h3>\n<p>Throughput is almost always lower than bandwidth. Protocol overhead, network congestion, hardware limits, and packet loss all cut into the actual flow of data.</p>\n<p>You might have a 1 Gbps link while your application only reaches 200 Mbps because of internal processing delays or inefficient code. To see what that gap means for a real file, try my <a href=\"https://ansezz.com/tools/transfer-time-calculator/\">transfer time calculator</a>.</p>\n<p>Measuring throughput gives you the real picture of your system health. It tells you how your Shopify store or your Laravel backend actually performs under load.</p>\n<p>If your throughput is far lower than your bandwidth, you have a bottleneck that more \"lanes\" will not fix.</p>\n<figure>\n\nRun `iperf3` between your app server and your database server to measure raw network throughput. Then time a real query or file transfer over the same path. The gap between the two numbers is your application overhead.\n\n<h2>The invisible hand of latency</h2>\n<p>You cannot discuss bandwidth and throughput without latency. Latency is the time it takes for data to travel from the source to the destination; the full trip there and back is the round-trip time (RTT).</p>\n<p>It is the \"lag\" you feel in a video call or a gaming session.</p>\n<h3>Wide pipe, slow trip</h3>\n<p>High bandwidth does not mean low latency. Consider a geostationary satellite link. It may have massive bandwidth (great for downloading large files), but the round trip is roughly 500ms because the signal has to climb to about 35,786 km and back.</p>\n<p>For a \"chatty\" application that fires many sequential calls, like a <a href=\"https://ansezz.com/blog/picking-the-right-rag-stack/\">RAG-based AI system</a> chaining retrieval and generation, high latency kills throughput.</p>\n<p>Even if you have infinite bandwidth, your throughput is capped by how many round trips your data has to make.</p>\n<p>Every time your application waits for an acknowledgment from the server, it sits idle. This is why optimizing for latency often matters more than upgrading your network plan.</p>\n\nCount the sequential network calls in your slowest request (database, cache, external APIs). Multiply by the round-trip time to each. That number is a floor no bandwidth upgrade can lower.\n\n<h2>Why bandwidth vs throughput matters for e-commerce</h2>\n<p>In a Shopify Plus environment, the gap between bandwidth and throughput directly affects conversion rates. When a customer hits your store, their browser makes dozens of requests for images, scripts, and API data.</p>\n<p>If your images are unoptimized, they eat up bandwidth. If your Shopify apps are poorly coded, they add latency. The result is lower throughput: the data does not reach the customer fast enough.</p>\n<p>You can have the fastest hosting in the world, but if your <a href=\"https://ansezz.com/blog/shopify-liquid-vs-headless/\">liquid templates</a> do heavy lifting on every page load, the actual delivery speed stalls.</p>\n<h3>Throughput is user experience</h3>\n<p>For businesses scaling their digital presence, focusing on throughput means focusing on the user experience. It means smaller payloads and fewer round trips.</p>\n<p>It is about making sure the \"cars\" on your highway move at top speed and arrive without delay.</p>\n<h2>Bottlenecks in the Laravel and DevOps stack</h2>\n<p>In a typical Laravel environment managed with tools like Docker or <a href=\"https://ansezz.com/blog/scaling-with-coolify/\">Coolify</a>, throughput bottlenecks often hide in the database or the cache layer.</p>\n<p>You might have a 10 Gbps internal network between your app server and your database server. On paper, your bandwidth is huge.</p>\n<h3>Slow queries look like slow networks</h3>\n<p>If your queries are not indexed, or you fetch thousands of unnecessary rows, your throughput will tank. The network is fast, but the application is slow to process and deliver the data.</p>\n<p>This is where clean <a href=\"https://ansezz.com/blog/category/architecture/\">architecture</a> and efficient query design become network optimization tools.</p>\n<h3>Deployments have the same gap</h3>\n<p>DevOps engineers also face throughput issues during CI/CD deployments. Moving a large Docker image across a network takes bandwidth. The time it takes to unpack, layer, and verify that image is a throughput concern.</p>\n<p>Optimizing your Dockerfiles to reduce layer size is a direct way to improve the \"actual flow\" of your deployment pipeline.</p>\n\nRun `docker history ` on your largest image and look for the biggest layers. Moving one heavy install into a multi-stage build often shrinks the image more than any network change.\n\n<h2>Optimizing for performance: a practical approach</h2>\n<p>Improving network performance needs two things: manage your capacity and optimize your flow. Here are the technical levers you can pull:</p>\n<ol>\n<li><strong>Compression and minification</strong>: Smaller payloads mean fewer bytes and fewer packets for the same content, so more useful data gets through the pipe. Use Gzip or Brotli for your web assets.</li>\n<li><strong>CDN implementation</strong>: Moving data closer to the user reduces latency. Shorter distances mean faster round trips and higher throughput, see <a href=\"https://ansezz.com/blog/cdn-vs-cache/\">CDN vs cache</a> for where each layer belongs.</li>\n<li><strong>Connection pooling</strong>: Reusing existing connections for database or API calls removes the overhead of the \"three-way handshake\" required for every new TCP connection.</li>\n<li><strong>Asynchronous processing</strong>: In Laravel, <a href=\"https://ansezz.com/blog/scaling-with-rabbitmq/\">using queues</a> lets you handle heavy tasks in the background. This keeps your main application throughput high for the end-user.</li>\n<li><strong>Protocol upgrades</strong>: HTTP/2 multiplexes many requests over one TCP connection. HTTP/3 goes further, running over QUIC to remove transport-level head-of-line blocking, so a single lost packet no longer stalls every other stream. On lossy mobile links the throughput gain is real.</li>\n</ol>\n\nOpen your site in the browser dev tools, Network tab, and add the \"Protocol\" column. Any `http/1.1` rows on your own domains are an easy upgrade.\n\n<h2>Measuring the right metrics</h2>\n<p>Stop treating your ISP's speed test as the only measure of success. To understand your system, monitor both bandwidth utilization and application throughput. Tools like Google Cloud Monitoring or Datadog give detailed insight into both.</p>\n<p>Look for places where available bandwidth is high but delivered throughput is low. These gaps are where your technical debt lives. They point to inefficient protocols, unoptimized assets, or server-side delays.</p>\n<p>Closing this gap is how you get a \"snappy\" feel for your web applications.</p>\n<h3>Architecture beats raw speed</h3>\n<p>A well-designed system on a moderate 100 Mbps link will often outperform a bloated system on a 1 Gbps link. Focus on the efficiency of the data flow rather than the width of the pipe.</p>\n<figure>\n\n- **Bandwidth is capacity**: It is the maximum potential of your network link, not the actual speed of your data.\n- **Throughput is reality**: It is the successful delivery rate of data, always limited by overhead and congestion.\n- **Latency is the silent killer**: Even with high bandwidth, high latency will cap your throughput by forcing wait times.\n- **Optimize the payload**: Smaller data sizes and fewer round trips are the most effective ways to increase throughput.\n- **Monitor the gap**: Use dashboard metrics to identify when your actual performance falls far below your theoretical capacity.\n- **Protocol choice matters**: A modern transport like HTTP/3 raises effective throughput over lossy links; query layers like GraphQL help separately by cutting over-fetching and round trips.\n\n<p>If you're chasing a throughput bottleneck in production, <a href=\"https://ansezz.com/services/\">here's how I help teams ship the fix</a>.</p>\n<p>If you had to choose between doubling your bandwidth or halving your latency for a real-time AI application, which would give the better return for your infrastructure?</p>\n</figure></figure>",
      "date_published": "2026-06-20T00:00:00.000Z",
      "date_modified": "2026-06-20T00:00:00.000Z",
      "tags": [
        "devops",
        "networking",
        "performance",
        "devops",
        "laravel",
        "shopify"
      ],
      "image": "https://ansezz.com/blog/bandwidth-vs-throughput/hero-06dd7eb4.webp"
    },
    {
      "id": "https://ansezz.com/blog/container-vs-pod/",
      "url": "https://ansezz.com/blog/container-vs-pod/",
      "title": "Container vs pod: the building blocks",
      "summary": "A container is the package; a Pod is the execution environment. How Kubernetes Pods share networking and storage, and when sidecars earn their keep.",
      "content_html": "<p>A container is the package. A Pod is the place it runs, and the difference decides whether your sidecars can even reach your app.</p>\n\n- A container is one isolated process with its own filesystem. A Pod is the smallest unit Kubernetes deploys, and it can hold one or more containers.\n- Containers in the same Pod share one IP address, talk over `localhost` and can share Volumes.\n- The sidecar pattern uses that sharing to add logging, proxies or config reloads without touching your app code.\n\n<p>When you move from a simple Docker setup to an orchestrated environment like Kubernetes, the terminology starts to blur.</p>\n<p>You wonder why you can't just deploy a container directly, and why Kubernetes insists on wrapping your perfectly good Docker image in something called a Pod. The container vs Pod distinction is the first thing that trips people up.</p>\n<p>Treat a Pod as merely a \"fancy container\" and you miss the shared networking and storage that make Kubernetes work.</p>\n<p>You end up with monolithic containers that are hard to scale, or sidecars that can't actually reach the main application. Understanding how a container relates to a Pod is the difference between a brittle deployment and a scalable, resilient system.</p>\n<h2>The atomic unit: what is a container?</h2>\n<p>A container is a lightweight, standalone executable package. It includes everything needed to run an application: code, runtime, system tools, system libraries, and settings.</p>\n<p>At its core, a container is an isolated process running on a host operating system. It shares the host's kernel but keeps its own filesystem and environment variables.</p>\n<h3>Consistent, but solitary</h3>\n<p>In the world of <a href=\"https://ansezz.com/blog/coolify-docker-saas-hosting/\">DevOps and Docker</a>, containers solved the \"it works on my machine\" problem. They give you a consistent environment from development to production.</p>\n<p>Whether you are running a Laravel backend or a Shopify app extension, the container stays the same.</p>\n<p>However, a container is solitary by nature. By default, it has its own IP address and its own isolated filesystem. Communication between two containers usually requires explicit networking configuration or external links.</p>\n<h2>The logical host: why Kubernetes needs Pods</h2>\n<p>Kubernetes does not manage containers directly. This is a common point of friction for beginners. Instead, it manages Pods.</p>\n<p>A Pod is the smallest deployable unit in Kubernetes. Think of a Pod as a \"logical host\" for one or more containers.</p>\n<h3>The wrapper</h3>\n<p>The Pod acts as a wrapper. It provides a shared execution environment for the containers inside it.</p>\n<p>Most Pods contain a single container, but the ability to group several is a core feature. These containers are always co-located and co-scheduled. They run on the same physical or virtual machine within the cluster.</p>\n<p>This coupling lets them behave as if they were processes on the same server, while each stays isolated in its own container.</p>\n<figure>\n\nRun `kubectl get pod  -o jsonpath='{.spec.containers[*].name}'` on a few of your Pods. You will see how many of them are really one container, and which already carry a helper.\n\n<h2>Shared network and localhost: one IP per Pod</h2>\n<p>The biggest technical advantage of a Pod is the shared network namespace. Every Pod gets a unique IP address within the Kubernetes cluster. Every container inside that Pod shares this IP address.</p>\n<p>This changes how services communicate. In a standard Docker setup, Container A needs to know the IP of Container B to send a request. Inside a Kubernetes Pod, Container A can talk to Container B simply by using <code>localhost</code>.</p>\n\n  <div>Each has its own IP. Container A needs Container B's address and explicit networking to reach it.</div>\n  <div>Both share the Pod's IP. Container A calls `localhost` and the port Container B listens on.</div>\n\n<h3>What that means in practice</h3>\n<ol>\n<li><strong>Port management</strong>: Since they share an IP, containers in the same Pod cannot bind to the same port. If one container uses port 8080, another container in that Pod must use a different port. To check which service usually owns a port, see my <a href=\"https://ansezz.com/tools/common-ports/\">common ports list</a>.</li>\n<li><strong>Performance</strong>: Communication over <code>localhost</code> is extremely fast. There is no need for complex routing or service discovery between the containers within the same Pod.</li>\n<li><strong>Simplicity</strong>: You can build modular systems where a helper container provides a service to the main application without exposing that service to the rest of the cluster.</li>\n</ol>\n\nBefore you add a second container to a Pod, list the ports each one binds. Any overlap will make the second container fail to start.\n\n<h2>Shared storage: passing files between containers</h2>\n<p>Just as they share a network, containers in a Pod can share storage. A container's internal filesystem is ephemeral and isolated, but a Pod can define shared Volumes.</p>\n<p>These volumes are mounted into the filesystem of every container that needs them.</p>\n<p>This is essential for applications that pass data between processes. For example, a main application might write log files, while a separate log-processing container reads those files and ships them to a central server.</p>\n<p>Without the Pod abstraction, you would have to deal with complex network mounts or external storage providers. In a Pod, it is as simple as mounting a shared directory.</p>\n<h2>The sidecar pattern: real-world multi-container Pods</h2>\n<p>The \"sidecar pattern\" is the main reason we use multi-container Pods.</p>\n<p>The \"sidecar\" container sits next to the \"main\" container and performs a supporting task.</p>\n<p>A sidecar lets you add functionality to an application without changing the application code.</p>\n<h3>Common sidecars</h3>\n<ul>\n<li><strong>Logging agents</strong>: A sidecar that watches a log file on a shared volume and sends the data to Elasticsearch or CloudWatch.</li>\n<li><strong>Proxies</strong>: A service mesh sidecar (like Istio or Envoy) that handles all incoming and outgoing network traffic for the main application.</li>\n<li><strong>Config reloaders</strong>: A sidecar that watches a configuration source (like a Git repo or ConfigMap) and signals the main application to reload when changes occur.</li>\n<li><strong>Security</strong>: A sidecar that handles SSL/TLS termination so the main application only has to deal with plain HTTP.</li>\n</ul>\n<p>By separating these concerns into different containers within the same Pod, you keep your main application container clean and focused. This modularity is a pillar of <a href=\"https://ansezz.com/blog/docker-vs-kubernetes/\">containers versus orchestration</a> thinking.</p>\n<h3>Native sidecars</h3>\n<p>Historically you added a sidecar by listing a second entry under <code>spec.containers</code>, which gave you no control over startup or shutdown order.</p>\n<p>Kubernetes now has first-class sidecar support. An init container with <code>restartPolicy: Always</code> starts before the main containers, stays running for the Pod's lifetime, and shuts down after them.</p>\n<p>This feature reached stable in Kubernetes v1.33, so reach for native sidecars over plain co-containers when ordering matters.</p>\n<figure>\n\nIf you have a log shipper or proxy that must be up before your app starts, move it from `spec.containers` to `spec.initContainers` with `restartPolicy: Always`.\n\n<h2>Technical comparison: container vs Pod</h2>\n<table>\n<thead>\n<tr>\n<th>Feature</th>\n<th>Standalone Container</th>\n<th>Containers in a Pod</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Unit of Deployment</strong></td>\n<td>Single process image</td>\n<td>Kubernetes Pod object</td>\n</tr>\n<tr>\n<td><strong>Networking</strong></td>\n<td>Unique IP per container</td>\n<td>Unique IP per Pod (shared)</td>\n</tr>\n<tr>\n<td><strong>Localhost Access</strong></td>\n<td>Isolated to the container</td>\n<td>Shared across all containers</td>\n</tr>\n<tr>\n<td><strong>Storage</strong></td>\n<td>Isolated filesystem</td>\n<td>Shared Volumes available</td>\n</tr>\n<tr>\n<td><strong>Scheduling</strong></td>\n<td>Manual or via runtime</td>\n<td>Automatic by K8s scheduler</td>\n</tr>\n<tr>\n<td><strong>Lifecycle</strong></td>\n<td>Tied to the process</td>\n<td>Tied to the Pod's status</td>\n</tr>\n<tr>\n<td><strong>Scaling</strong></td>\n<td>Scale individual instances</td>\n<td>Scale the entire Pod unit</td>\n</tr>\n</tbody>\n</table>\n<h2>DevOps deployment: GCP and AWS context</h2>\n<p>When you move your workloads to managed cloud providers like Google Cloud (GKE) or AWS (EKS), the container vs Pod distinction becomes operational. These platforms manage the infrastructure, but you still define the Pod specifications.</p>\n<h3>Serverless blurs the line</h3>\n<p>EKS on Fargate still runs real Pods, so multi-container Pods and sidecars work exactly as they do on a self-managed node. Fargate just provisions the compute per Pod instead of per node.</p>\n<p>Cloud Run started as single-container only. Since May 2023 (first in preview, now generally available) it supports multi-container deployments where sidecars share the network namespace and talk over <code>localhost</code>, just like a Pod.</p>\n<p>So you no longer have to jump to full Kubernetes the moment you need a sidecar. You do once you need <a href=\"https://ansezz.com/blog/serverless-vs-containers/\">richer orchestration</a> like DaemonSets, complex scheduling, or fine-grained networking.</p>\n\nWrite down the one sidecar you need today. If it is a single proxy or log agent, check whether Cloud Run or Fargate already covers it before you stand up a cluster.\n\n\n- A container is the packaging format, while a Pod is the execution environment.\n- Containers in a Pod share the same IP address and can communicate via `localhost`.\n- Shared Volumes allow containers within a Pod to exchange data through the filesystem.\n- The sidecar pattern uses multiple containers in one Pod to separate concerns like logging and security.\n- Kubernetes schedules and scales Pods as a single unit, ensuring all containers land on the same node.\n- Understanding Pods matters when you manage complex deployments on GCP, AWS, or [self-hosted tools like Coolify](/blog/coolify-self-hosted-saas/).\n\n<p>If you're architecting workloads that have outgrown plain Docker, <a href=\"https://ansezz.com/services/\">here's how I help teams ship it</a>.</p>\n<p>At what point does the overhead of managing a multi-container Pod outweigh the benefits of process separation in your current infrastructure?</p>\n</figure></figure>",
      "date_published": "2026-06-20T00:00:00.000Z",
      "date_modified": "2026-06-20T00:00:00.000Z",
      "tags": [
        "devops",
        "kubernetes",
        "docker",
        "devops",
        "infrastructure"
      ],
      "image": "https://ansezz.com/blog/container-vs-pod/hero-7431db84.webp"
    },
    {
      "id": "https://ansezz.com/blog/ai-vs-machine-learning/",
      "url": "https://ansezz.com/blog/ai-vs-machine-learning/",
      "title": "AI vs machine learning: an engineering deep dive",
      "summary": "AI is the umbrella, machine learning is the engine. See how the distinction shapes your RAG pipelines, agentic systems, and Laravel or Shopify architecture.",
      "content_html": "<p>Rules you write by hand can be AI. Patterns a model learns from data are machine learning. Mix them up and you build the wrong system.</p>\n\n- AI is the umbrella: any system that reasons, plans or decides, including plain hardcoded rules. Machine learning is one way to get there, by training a model on data.\n- RAG pipelines and agents are hybrids: ML models (embeddings, LLMs) inside an AI system you design with code and prompts.\n- The split shapes your stack: orchestration code runs on normal servers, while ML models need data pipelines, GPUs and drift monitoring.\n\n<p>The tech industry often treats \"AI\" and \"machine learning\" as interchangeable. This confusion leads to real architectural debt when teams build intelligent features without understanding the underlying mechanics.</p>\n<p>You end up trying to build a predictive engine out of static rule-based systems or, the other way around, over-engineering a simple decision tree with a neural network.</p>\n<p>Picking the wrong tool produces bloated cloud bills, slow performance, and systems that fail to generalize to real-world data. When you mix up the umbrella field of artificial intelligence with the specific mechanics of machine learning, you lose the ability to choose the most efficient path for your product.</p>\n<p>This guide draws the line between the two concepts and shows how they show up in modern stacks like Laravel and Shopify, with a focus on RAG and agentic systems. The goal: a clear framework for deciding when to reach for rule-based logic and when to deploy a learned model.</p>\n<h2>The AI umbrella: broad intelligent systems</h2>\n<p>Artificial Intelligence (AI) is the broad field of creating systems that can perform tasks that usually require human intelligence. This is the \"umbrella\" term.</p>\n<p>It covers everything from simple \"if-then\" logic to the most complex large language models (LLMs).</p>\n<h3>AI does not have to learn</h3>\n<p>In engineering terms, AI is about the system's behavior. If a software system can reason, plan, or solve problems, it is an AI system. This does not mean it has to \"learn.\"</p>\n<p>Early AI systems, often called symbolic AI or expert systems, relied on hardcoded rules and logic. These systems are very predictable and easy to debug because every decision path is written out explicitly.</p>\n<h3>A rule-based example</h3>\n<p>Take a sophisticated shipping calculator in a Shopify Plus store that adjusts prices based on complex regional taxes and carrier API responses. It mimics human decision-making from a set of logical parameters, which puts it at the simple, rule-based end of AI.</p>\n<p>It is not machine learning, because it does not improve its performance based on past data unless a developer updates the code.</p>\n<figure>\n\nPick one \"smart\" feature in your product and ask: does it get better from data, or only when someone edits the code? If it is the second, it is rule-based, and you can test it like normal code.\n\n<h2>Machine learning: the engine of pattern recognition</h2>\n<p>Machine Learning (ML) is a subset of AI focused on algorithms that let computers learn from data and make predictions. Instead of being explicitly programmed for a task, an ML model is \"trained\" on a dataset to find patterns and hidden structures.</p>\n<h3>When rules run out</h3>\n<p>When your product needs to handle high-dimensional data where rules are too complex to write by hand, ML is the solution. Think about image recognition or fraud detection.</p>\n<p>You cannot write enough \"if\" statements to account for every possible pixel arrangement in a photo of a cat. Instead, you feed an ML model thousands of images, and it learns the statistical representation of a \"cat.\"</p>\n\n  <div>A developer writes every decision path. Predictable, easy to debug, and it only improves when someone changes the code.</div>\n  <div>A model learns the patterns from data. Handles inputs too complex for rules, and needs data, training and evaluation to improve.</div>\n\n<h3>A different lifecycle</h3>\n<p>In production, ML involves a lifecycle that is distinct from traditional software development. It requires data collection, cleaning, feature engineering, model training, and evaluation.</p>\n<p>You can read more about how this differs from standard workflows in our post on <a href=\"https://ansezz.com/blog/ai-vs-traditional-development/\">AI vs traditional development</a>. For the deeper discriminative-vs-generative comparison inside ML itself, see <a href=\"https://ansezz.com/blog/ml-vs-genai/\">machine learning vs generative AI</a>.</p>\n<h2>AI vs machine learning in RAG systems</h2>\n<p>The distinction becomes critical when you move into modern AI engineering, particularly with Retrieval-Augmented Generation (RAG). A RAG system is a clear example of an AI system that uses several ML components to reach a goal.</p>\n<p>In a RAG pipeline, the system behavior is the AI part. How a user query is received, how a search runs against a vector database, and how the results go into a prompt is a design problem. The components doing the heavy lifting, however, are ML models.</p>\n<h3>Embedding models and vector databases</h3>\n<p>The \"retrieval\" part of RAG relies on embedding models. These are ML models (specifically deep learning models) that turn text into high-dimensional vectors.</p>\n<p>When you store these vectors in a tool like pgvector or a dedicated vector database, you are using the output of a machine learning process.</p>\n<h3>The generator (LLM)</h3>\n<p>The \"generation\" part is handled by a large language model. This is a massive ML model trained on trillions of tokens of text.</p>\n<p>Whether you call it through an API or a local deployment, the model itself is a static file of weights learned through intensive training.</p>\n<h3>Which job are you doing?</h3>\n<p>When you refine your RAG system by changing the chunking strategy or the system prompt, you are doing AI engineering.</p>\n<p>You are only doing ML engineering if you decide to fine-tune the embedding model or the LLM itself on your domain data, a trade-off covered in <a href=\"https://ansezz.com/blog/rag-vs-fine-tuning/\">RAG vs fine-tuning</a>. Understanding these layers helps prevent <a href=\"https://ansezz.com/blog/7-rag-mistakes-production/\">common RAG mistakes in production</a>.</p>\n<p>You are only doing ML engineering if you decide to fine-tune the embedding model or the LLM itself.</p>\n\nLook at the last three changes you made to your RAG pipeline. Label each one \"AI engineering\" (chunking, prompts, routing) or \"ML engineering\" (training, fine-tuning). Most teams find nearly all of them are the first kind.\n\n<h2>Agentic systems and autonomous workflows</h2>\n<p>Agentic systems take the AI vs machine learning comparison a step further. An \"agent\" is an AI system that uses an ML model as its \"brain\" to decide a sequence of actions toward a goal. The jump from a static model to an autonomous one is the focus of <a href=\"https://ansezz.com/blog/llm-vs-ai-agent/\">LLM vs AI agent</a>.</p>\n<p>In an agentic workflow, the ML model (the LLM) is just one tool in the agent's belt. The agent might also have access to a search engine, a calculator, or a database. The logic that handles the \"loop\" (Observe -&gt; Think -&gt; Act) is the AI system design.</p>\n<h3>Agents in commerce</h3>\n<p>For developers working with Shopify, <a href=\"https://ansezz.com/blog/agentic-commerce-shopify/\">agentic commerce</a> allows for autonomous customer service or inventory management.</p>\n<p>The agent uses machine learning to understand the customer's intent (NLP) but uses traditional software APIs to execute a refund or check stock levels.</p>\n<pre><code>// A conceptual example of an AI Agent 'thought' process in a Laravel controller\npublic function handleAgentRequest(Request $request)\n{\n    $userInput = $request-&gt;input('query');\n\n    // ML Component: Identify intent using an LLM\n    $intent = $this-&gt;llmService-&gt;identifyIntent($userInput);\n\n    // AI System Logic: Choose the tool based on intent\n    if ($intent === 'check_order_status') {\n        return $this-&gt;orderService-&gt;getStatus($request-&gt;user());\n    }\n\n    // AI System Logic: Fallback to RAG if intent is informational\n    return $this-&gt;ragService-&gt;queryKnowledgeBase($userInput);\n}\n</code></pre>\n<figure>\n<h2>Practical implementation: Laravel and Shopify contexts</h2>\n<p>Integrating AI and ML into existing frameworks like Laravel or Shopify requires a clear understanding of where the data lives and how the models are served.</p>\n<h3>Laravel and ML orchestration</h3>\n<p>Laravel is an excellent framework for building the \"AI system\" around an ML model. You can use Laravel's queue system to handle long-running ML tasks, or its HTTP client to talk to Python-based ML microservices.</p>\n<p>Many developers use Laravel to manage the \"human-in-the-loop\" side of AI, such as reviewing model outputs before they are published.</p>\n<h3>Shopify and AI</h3>\n<p>Shopify has built AI features directly into its platform through Shopify Magic. For custom Shopify Plus builds, you might implement your own ML-driven product recommendation engine.</p>\n<p>This involves capturing user behavior data, training a model (likely off-platform on Google Cloud or AWS), and serving those recommendations through a custom app or a Liquid block.</p>\n\nPut a queue between your Laravel app and any model call that can take more than a second. Dispatch a job, store the result, and let the UI poll or listen for it.\n\n<h2>Cloud infrastructure for AI and machine learning</h2>\n<p>Deploying these systems requires different infrastructure strategies. AI systems (the orchestration code) can usually run on standard web servers or serverless functions.</p>\n<p>Machine learning models, especially for training or high-throughput inference, often require GPUs or specialized hardware.</p>\n<h3>Separate containers</h3>\n<p>Docker and tools like Coolify make it simpler to deploy these modular systems. You might host your Laravel application on a standard VPS while your vector database and ML inference server run in separate containers. For more on this, check out our guide on <a href=\"https://ansezz.com/blog/coolify-docker-saas-hosting/\">Coolify and Docker for SaaS hosting</a>.</p>\n<h3>Monitor different things</h3>\n<p>Monitoring is also different. For an AI system, you monitor latency and error rates. For an ML model, you monitor \"drift\" (when the real-world data starts to look different from the training data) and accuracy.</p>\n<table>\n<thead>\n<tr>\n<th>What you run</th>\n<th>Where it runs</th>\n<th>What you monitor</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>AI system (orchestration code)</strong></td>\n<td>Standard web servers or serverless</td>\n<td>Latency and error rates</td>\n</tr>\n<tr>\n<td><strong>ML model</strong></td>\n<td>GPUs or specialized hardware</td>\n<td>Drift and accuracy</td>\n</tr>\n</tbody>\n</table>\n\nAdd one drift check to a model you own: compare this week's input distribution (length, language, category mix) with the data it was trained or tuned on.\n\n\nTelling AI and machine learning apart is about choosing the right architecture and team skill sets for your project, not about vocabulary.\n<ul>\n<li><strong>AI is the \"what\":</strong> It describes the goal of creating intelligent behavior. It includes both hardcoded logic and learned models.</li>\n<li><strong>Machine Learning is the \"how\":</strong> It is a specific method for achieving AI by training models on data.</li>\n<li><strong>RAG and agents are hybrid systems:</strong> They use ML components (LLMs, embeddings) inside an AI system designed with code and prompts.</li>\n<li><strong>Framework integration matters:</strong> Laravel is great for AI orchestration, while Shopify Plus provides a solid environment for ML-driven commerce.</li>\n<li><strong>Infrastructure needs differ:</strong> AI code is lightweight. ML models require specialized environments for training and inference.\n</li></ul>\n\n<p>Are you building a system that needs to follow strict regulatory rules, or one that needs to adapt to unpredictable user behavior?</p>\n</figure></figure>",
      "date_published": "2026-06-19T00:00:00.000Z",
      "date_modified": "2026-06-19T00:00:00.000Z",
      "tags": [
        "ai",
        "ai",
        "machine-learning",
        "rag",
        "agentic-ai",
        "laravel",
        "shopify"
      ],
      "image": "https://ansezz.com/blog/ai-vs-machine-learning/hero-d17beeaf.webp"
    },
    {
      "id": "https://ansezz.com/blog/training-vs-inference/",
      "url": "https://ansezz.com/blog/training-vs-inference/",
      "title": "Training vs inference: scaling AI systems",
      "summary": "How training and inference differ in compute, cost, and hardware, and how to architect each phase so your AI app stays fast and affordable at scale.",
      "content_html": "<p>Training is the expensive class you take once. Inference is the bill you pay on every single request.</p>\n\n- Training adjusts a model's weights over a huge dataset. It is compute-heavy, runs on GPU clusters and happens once or now and then.\n- Inference is a forward pass with frozen weights. It is cheaper per request, but it runs every time a user asks, so it usually becomes the bigger cost over time.\n- Size each phase on its own: throughput and big interconnects for training, latency, quantization and smaller hardware for inference, plus RAG to add new knowledge without retraining.\n\n<p>Most engineering teams treat AI models as a single black box. They allocate a massive GPU budget and hope for the best.</p>\n<p>But when the application hits production, latency spikes, costs spiral, and the \"intelligent\" features start timing out.</p>\n<p>This happens because developers fail to separate the two phases of an AI lifecycle: training and inference. Without a clear picture of how each stage consumes resources, your AI strategy is a shot in the dark.</p>\n<h2>The anatomy of training: learning from data</h2>\n<p>Training is the \"learning\" phase of a machine learning model. During this stage, you feed an algorithm a massive dataset.</p>\n<p>The model looks at the data, makes a guess, compares its guess to the actual answer, and adjusts its internal parameters (weights) to get closer to the truth next time. This process is repeated millions or even billions of times.</p>\n<h3>Why it is so compute-heavy</h3>\n<p>The main goal of training is to minimize error. Because this requires massive matrix multiplications and constant backpropagation, it is extremely compute-intensive.</p>\n<p>You are asking a computer to solve a giant calculus problem over and over again. This is why training usually happens on large clusters of high-end GPUs or TPUs.</p>\n<h3>A burst, then frozen</h3>\n<p>Training is usually a burst-heavy, high-upfront-cost activity. You might spend two weeks and $50,000 to train a custom model for your specific industry.</p>\n<p>Once the model reaches a satisfactory level of accuracy, the training phase ends. The model's weights are \"frozen,\" and it is ready to be used in the real world.</p>\n<figure>\n<h2>The power of inference: applying knowledge</h2>\n<p>Inference is the \"execution\" phase. This is what happens when a user interacts with your application.</p>\n<p>A customer types a query into your Shopify store's AI assistant, the model processes that query using its frozen weights, and it returns an answer. No learning happens during inference. The model simply applies what it already knows.</p>\n<h3>A forward pass</h3>\n<p>In technical terms, inference is a \"forward pass.\" You provide an input, the data flows through the layers of the model, and an output is generated.</p>\n<p>There is no backpropagation and no weight updates. This makes inference much faster and less compute-intensive than training on a per-request basis.</p>\n<h3>The unbounded cost</h3>\n<p>However, inference is where the \"unbounded cost\" problem lives. Training is a one-time or periodic expense. Inference happens every time a user makes a request.</p>\n<p>If you have a million users making ten queries a day, you are running ten million inferences. Over the lifetime of a successful product, inference costs often end up far larger than training costs.</p>\n\nEstimate your monthly inference volume: daily active users, times queries per user, times 30. Multiply by your cost per request. Compare that with what the model cost to train or fine-tune.\n\n<h2>The classroom metaphor: student vs. graduate</h2>\n<p>To make these ideas simpler, think of a student in medical school.</p>\n\n  <div>\n<p><strong>The years of study.</strong> The student reads thousands of textbooks, attends lectures, and takes practice exams. This process is slow, expensive, and requires intense focus. The \"parameters\" of the student's brain are being adjusted as they learn how to diagnose diseases.</p>\n  </div>\n  <div>\n<p><strong>The doctor in the clinic.</strong> A patient walks in with symptoms. The doctor uses existing knowledge to give a diagnosis and does not go back to medical school for every patient. The diagnosis happens in minutes, not years.</p>\n  </div>\n\n<p>If you are building an <a href=\"https://ansezz.com/blog/ai-vs-traditional-development/\">AI vs traditional development</a> strategy, you need to decide if you are training a new doctor (custom training) or simply hiring one that already exists (using a pre-trained model via API).</p>\n<h2>Hardware and infrastructure: GPUs vs. the rest</h2>\n<p>The hardware requirements for these two phases are very different. Understanding this can save you thousands in infrastructure costs.</p>\n<table>\n<thead>\n<tr>\n<th>Feature</th>\n<th>Training</th>\n<th>Inference</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Primary Goal</strong></td>\n<td>High Throughput</td>\n<td>Low Latency</td>\n</tr>\n<tr>\n<td><strong>Compute Pattern</strong></td>\n<td>Burst-heavy / Parallel</td>\n<td>Steady / Sequential</td>\n</tr>\n<tr>\n<td><strong>Hardware</strong></td>\n<td>Multi-GPU Clusters (H100, A100)</td>\n<td>Single GPU, CPU, or Edge (T4, L4, Apple Silicon)</td>\n</tr>\n<tr>\n<td><strong>Optimization</strong></td>\n<td>Gradient Descent / Backprop</td>\n<td>Quantization / Model Compression</td>\n</tr>\n<tr>\n<td><strong>Cost Type</strong></td>\n<td>CAPEX (Upfront)</td>\n<td>OPEX (Recurring)</td>\n</tr>\n</tbody>\n</table>\n<h3>Different priorities</h3>\n<p>For training, you need massive VRAM and high-speed interconnects (like NVLink) between GPUs. For inference, you often prioritize energy efficiency and \"cost per token.\"</p>\n<p>In many cases, a well-optimized model can run inference on standard CPUs or specialized \"edge\" chips, which is much cheaper than keeping a fleet of high-end GPUs.</p>\n<p>If you manage your own servers, you can split the workloads: put training tasks on high-performance clusters and move inference to smaller, distributed nodes closer to your users.</p>\n\nTry a quantized version (8-bit or 4-bit) of the model you serve on your smallest available GPU or a CPU. Measure latency and answer quality on 20 real prompts before paying for bigger hardware.\n\n<h2>Integrating inference into Laravel applications</h2>\n<p>When building custom web solutions, you usually deal with the inference side of the equation. You are not training a frontier model from scratch.</p>\n<p>You are calling an API or a self-hosted model to perform a task. Here is a typical pattern for handling AI inference inside a Laravel controller.</p>\n<pre><code>namespace App\\Http\\Controllers;\n\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Facades\\Http;\n\nclass AIInferenceController extends Controller\n{\n    /**\n     * Handle an AI inference request.\n     */\n    public function generate(Request $request)\n    {\n        $prompt = $request-&gt;input('prompt');\n\n        // We use a high-performance inference endpoint\n        // This could be OpenAI, Anthropic, or a self-hosted vLLM server\n        $response = Http::withHeaders([\n            'Authorization' =&gt; 'Bearer ' . config('services.ai.key'),\n        ])-&gt;post('https://api.inference-provider.com/v1/completions', [\n            'model' =&gt; 'llama-3-70b',\n            'prompt' =&gt; $prompt,\n            'max_tokens' =&gt; 150,\n            'temperature' =&gt; 0.7,\n        ]);\n\n        if ($response-&gt;successful()) {\n            return response()-&gt;json([\n                'result' =&gt; $response-&gt;json('choices.0.text'),\n                'latency' =&gt; $response-&gt;header('X-Inference-Time'),\n            ]);\n        }\n\n        return response()-&gt;json(['error' =&gt; 'Inference failed'], 500);\n    }\n}\n</code></pre>\n<h3>Latency is the contract</h3>\n<p>This simple setup highlights a key architectural point: the <a href=\"https://ansezz.com/blog/api-gateway-ai-stack/\">API Gateway for your AI stack</a> must handle the latency needs of inference.</p>\n<p>Users expect an answer in seconds, not minutes, and the first token well under that. A slow gateway in front of a fast model still feels slow.</p>\n\nLog the time to first token and the total response time for every inference call, next to the model name. Those two numbers tell you whether to fix the model, the network, or the prompt size.\n\n<h2>The role of RAG: inference with a memory</h2>\n<p>One way to bridge the gap between training and inference is retrieval-augmented generation (RAG).</p>\n<p>Instead of re-training a model every time your data changes (which is expensive and slow), you give the model \"context\" during the inference phase. That tradeoff is its own decision, covered in <a href=\"https://ansezz.com/blog/rag-vs-fine-tuning/\">RAG vs fine-tuning</a>.</p>\n<h3>A temporary memory</h3>\n<p>In a RAG system, you search a vector database for relevant information and \"stuff\" it into the prompt. The model then uses its pre-trained reasoning to answer based on that new data. This is inference acting like it has a temporary memory.</p>\n<p>Be careful, though. If you don't optimize your vector search, your inference latency will explode. You can read more about avoiding <a href=\"https://ansezz.com/blog/7-rag-mistakes-production/\">RAG mistakes in production</a> to keep your systems lean.</p>\n<figure>\n\n- **Training is about learning.** It is a one-time or periodic compute-heavy process that builds the model's intelligence.\n- **Inference is about acting.** It is the real-time application of that intelligence and represents the majority of long-term costs.\n- **Hardware matters.** Don't use a massive GPU cluster for inference if a smaller, quantized model can run on a single T4 or even a CPU.\n- **Optimize for your phase.** If you are training, focus on throughput (tokens per second). If you are serving users, focus on latency (time to first token).\n- **RAG is your friend.** It allows you to give \"new knowledge\" to a frozen model during inference without the massive cost of re-training.\n\n<p>If you're architecting an AI stack that has to stay fast and affordable at scale, <a href=\"https://ansezz.com/services/\">here's how I help teams ship it</a>.</p>\n<p>At what point in your product's growth do you expect inference costs to exceed your initial development budget?</p>\n</figure></figure>",
      "date_published": "2026-06-18T00:00:00.000Z",
      "date_modified": "2026-06-18T00:00:00.000Z",
      "tags": [
        "ai",
        "llm",
        "llm-inference",
        "devops",
        "rag",
        "infrastructure",
        "ai-engineering"
      ],
      "image": "https://ansezz.com/blog/training-vs-inference/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/vector-search-vs-graph-search/",
      "url": "https://ansezz.com/blog/vector-search-vs-graph-search/",
      "title": "Vector search vs graph search for RAG",
      "summary": "Compare vector search and graph search for RAG. When to use embeddings via pgvector vs relationship-based knowledge graphs, and why GraphRAG often wins.",
      "content_html": "<p>Vector search finds things that are similar. Graph search finds things that are connected. Multi-hop questions need the second.</p>\n\n- Vector search (for example pgvector) turns text into embeddings and finds the nearest ones. It is easy to start with and great for fuzzy, semantic matching.\n- Graph search stores entities and their relationships, so it can follow chains of facts that vector search misses.\n- GraphRAG combines them: vectors find the starting points, graph traversal pulls in the connected context.\n\n<p>Retrieval-Augmented Generation (RAG) forces a question most teams put off: how is data actually retrieved?</p>\n<p>Most production systems start with vector search because it is accessible and effective for basic similarity. But as queries get more complex, the limits of semantic similarity show.</p>\n<p>A system that finds \"similar\" things often fails when asked to explain \"connected\" things. That gap is where the choice between vector search and graph search decides whether your RAG strategy holds up.</p>\n<p>Relying only on vector embeddings can lead to what is known as the semantic trap. You might retrieve five document chunks that all mention a topic, but if those chunks do not link to each other, the LLM will struggle to put together a multi-hop answer.</p>\n<p>This often results in hallucinations or incomplete reasoning. If your RAG pipeline cannot traverse relationships, it is working with a deck of shuffled cards rather than an organized map.</p>\n<p>The fix starts with understanding the trade-offs between vector-based and relationship-based retrieval. Vector search excels at high-dimensional similarity; graph search offers structured traversals that preserve the logic of your data.</p>\n<p>Combining the two into a hybrid architecture is usually the safest path forward.</p>\n<h2>Understanding vector search and embeddings</h2>\n<p>Vector search works by converting unstructured data into dense numerical arrays called embeddings. These embeddings represent the semantic meaning of the text in a multi-dimensional space.</p>\n<p>When a user submits a query, that query is also converted into a vector. The system then calculates the distance between the query vector and the stored vectors using measures like cosine similarity or Euclidean distance.</p>\n<h3>pgvector makes it accessible</h3>\n<p>For developers working within the Laravel or Node.js ecosystems, tools like pgvector have made this very accessible. By adding a vector column to a standard Postgres database, you can run similarity searches right next to your relational data.</p>\n<p>This approach is very efficient for \"fuzzy\" matching. It can find a product description for a \"crimson summer dress\" even if the query only mentions a \"red lightweight gown.\"</p>\n<h3>No sense of structure</h3>\n<p>However, vector search is limited by its lack of structural awareness. It treats every chunk of data as an independent point in space.</p>\n<p>It has no built-in understanding that \"User A\" is the \"CEO\" of \"Company B\" unless those words sit together in a single text chunk.</p>\n<figure>\n\nWrite down three real questions your users ask that need two facts from different documents to answer. Run them against your current retriever and check whether both facts come back.\n\n<h2>The mechanics of graph search</h2>\n<p>Graph search takes a different approach by modeling data as nodes (entities) and edges (relationships). Instead of looking for similarity in a coordinate system, graph search follows defined paths.</p>\n<p>A node might represent a person, a place, or a concept, while an edge defines how they relate. For example, an edge labeled \"works_at\" might connect a person node to a company node.</p>\n<h3>Multi-hop queries</h3>\n<p>This structure allows multi-hop queries that are nearly impossible for pure vector search. You can ask the system to \"find all employees at companies that use Laravel and were founded after 2015.\"</p>\n<p>A graph database like Neo4j or a graph-capable extension can follow these links precisely. In RAG, this means the retrieval engine can pull in a coherent chain of facts rather than a disjointed set of similar-sounding paragraphs.</p>\n<h3>The cost is ingestion</h3>\n<p>The challenge with graph search is the ingestion process. Vector search only requires an embedding model. Graph search requires entity extraction and schema mapping.</p>\n<p>You must identify which entities exist in your text and how they relate before you can store them in the graph. This adds complexity to your data pipeline but pays off in reasoning accuracy.</p>\n<p>A system that finds \"similar\" things often fails when asked to explain \"connected\" things.</p>\n<h2>Comparison: vector vs graph search</h2>\n<p>Choosing the right approach depends on the nature of your data and the questions your users ask. The table below outlines the main technical differences.</p>\n<table>\n<thead>\n<tr>\n<th>Feature</th>\n<th>Vector Search (pgvector)</th>\n<th>Graph Search (Knowledge Graph)</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Data Model</strong></td>\n<td>High-dimensional dense vectors</td>\n<td>Nodes, edges, and properties</td>\n</tr>\n<tr>\n<td><strong>Query Type</strong></td>\n<td>K-Nearest Neighbors (k-NN)</td>\n<td>Traversals and pattern matching</td>\n</tr>\n<tr>\n<td><strong>Strengths</strong></td>\n<td>Semantic similarity, fuzzy match</td>\n<td>Complex relationships, multi-hop logic</td>\n</tr>\n<tr>\n<td><strong>Weaknesses</strong></td>\n<td>Struggles with structural reasoning</td>\n<td>High ingestion complexity</td>\n</tr>\n<tr>\n<td><strong>Ideal For</strong></td>\n<td>FAQ bots, general search, broad Q&amp;A</td>\n<td>Fraud detection, supply chain, reasoning</td>\n</tr>\n<tr>\n<td><strong>Scaling</strong></td>\n<td>Efficient with ANN indexing (HNSW)</td>\n<td>Sensitive to graph density and depth</td>\n</tr>\n</tbody>\n</table>\n<p>For a deeper look at avoiding common pitfalls in this space, see our guide on <a href=\"https://ansezz.com/blog/7-rag-mistakes-production/\">7 RAG mistakes in production</a>. Many teams start with vectors and only introduce graphs when they hit a \"reasoning wall.\"</p>\n<h2>GraphRAG: the hybrid evolution</h2>\n<p>The most advanced RAG systems are moving toward a hybrid model often called GraphRAG. This architecture does not choose one over the other. It uses both to give the LLM richer context.</p>\n<p>In a GraphRAG setup, the system first runs a vector search to find relevant starting points in the knowledge base. From those starting points, it uses graph traversals to pull in related entities and their relationships.</p>\n\n  <div>Five chunks that all mention the topic, with no links between them. The LLM has to guess how they fit.</div>\n  <div>Vector search finds the entry points, then graph traversal adds the linked facts. The LLM gets a small, connected subgraph.</div>\n\n<h3>A worked example</h3>\n<p>Imagine a medical research application. A vector search might find a paper about a specific drug.</p>\n<p>The graph search then identifies the chemical compounds in that drug, the clinical trials associated with it, and the known side effects reported in other related papers.</p>\n<p>The context given to the LLM is a structured \"subgraph\" of knowledge. This reduces the risk of hallucinations because the LLM is working with explicitly linked facts.</p>\n<h3>Running the stack</h3>\n<p>This hybrid approach needs a solid DevOps foundation. Tools like Docker and Coolify can help manage the services involved, including your vector store, graph database, and the extraction services that keep them in sync.</p>\n<p>If you are exploring how to host these stacks, our article on <a href=\"https://ansezz.com/blog/coolify-self-hosted-saas/\">Coolify and self-hosted SaaS</a> is a good starting point for infrastructure management.</p>\n<figure>\n\nBefore building a full graph, extract only the two or three entity types your users ask about most (for example products and suppliers) and the one relationship between them. See whether that alone fixes your multi-hop questions.\n\n<h2>Technical implementation with pgvector and SQL</h2>\n<p>For teams already using Postgres, pgvector is the logical starting point for adding vector capabilities. It fits into existing SQL workflows.</p>\n<p>You can store your embeddings in a <code>vector</code> column and use the <code>&lt;-&gt;</code> (Euclidean) or <code>&lt;=&gt;</code> (cosine distance) operators to query them.</p>\n<pre><code>-- Example: Finding similar document chunks in Postgres\nSELECT content, 1 - (embedding &lt;=&gt; '[0.1, 0.2, 0.3, ...]') AS similarity\nFROM document_chunks\nORDER BY embedding &lt;=&gt; '[0.1, 0.2, 0.3, ...]'\nLIMIT 5;\n</code></pre>\n<h3>Let the index do its job</h3>\n<p>Order by the distance expression itself and keep the <code>LIMIT</code>. That is the shape pgvector's HNSW and IVFFlat indexes can use. To see how big those indexes get and how much RAM they need, try my <a href=\"https://ansezz.com/tools/pgvector-calculator/\">pgvector size calculator</a>.</p>\n<p>A <code>WHERE</code> filter on the similarity score (for example <code>&gt; 0.8</code>) cannot use the index and forces a full scan, so apply a score threshold to the five rows you get back instead.</p>\n<h3>When joins stop scaling</h3>\n<p>To approximate a graph without a dedicated graph database, you can model simple one-to-many or many-to-many relationships with relational joins.</p>\n<p>But as the number of hops grows, recursive joins and <code>WITH RECURSIVE</code> queries get expensive fast, and the planner struggles to keep them efficient.</p>\n<p>That is the point to introduce a specialized graph layer such as Neo4j or the Apache AGE extension, which adds openCypher graph queries directly to Postgres. For the broader question of which retrieval components to assemble, see <a href=\"https://ansezz.com/blog/picking-the-right-rag-stack/\">picking the right RAG stack</a>.</p>\n\nRun `EXPLAIN ANALYZE` on your similarity query. If you do not see an index scan on your HNSW or IVFFlat index, rewrite it to `ORDER BY embedding &lt;=&gt; ... LIMIT k`.\n\n<h2>Choosing your path</h2>\n<p>The decision between vector and graph search is not binary. It is a spectrum of technical complexity versus reasoning capability.</p>\n<p>For simple semantic search over a collection of PDFs, vector search with pgvector is almost always the right answer. It is fast, easy to implement, and needs little maintenance.</p>\n<h3>When the graph is worth it</h3>\n<p>If your application needs high precision, multi-step logic, or the ability to explain \"why\" a result was chosen, the investment in a knowledge graph is necessary.</p>\n<p>The overhead of building an extraction pipeline is the price you pay for a system that understands the relationships within your data.</p>\n<h3>Most systems land in the middle</h3>\n<p>Most production AI systems will eventually use vectors for broad discovery and graphs for deep reasoning. If you design your system to be modular today, your infrastructure is ready to evolve as your AI needs grow.</p>\n\n- Vector search is best for semantic similarity and is easy to implement using tools like pgvector.\n- Graph search excels at multi-hop reasoning and mapping complex relationships between entities.\n- Vector search treats data points as isolated, while graph search treats them as connected nodes.\n- GraphRAG is a hybrid approach that uses vector search for discovery and graph search for context expansion.\n- Start with vector search for most projects, but prepare for graph search if your queries require complex structural reasoning.\n- Monitor retrieval metrics like recall and precision to spot when a vector-only strategy starts failing.\n\n<p>If you're building a RAG pipeline for production, <a href=\"https://ansezz.com/services/\">here's how I help teams ship it</a>.</p>\n<p>How is your current RAG pipeline handling multi-hop queries that connect data points across documents?</p>\n</figure></figure>",
      "date_published": "2026-06-18T00:00:00.000Z",
      "date_modified": "2026-06-18T00:00:00.000Z",
      "tags": [
        "ai",
        "rag",
        "pgvector",
        "vector-search",
        "architecture",
        "ai"
      ],
      "image": "https://ansezz.com/blog/vector-search-vs-graph-search/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/terraform-vs-ansible/",
      "url": "https://ansezz.com/blog/terraform-vs-ansible/",
      "title": "Terraform vs Ansible: the real difference in DevOps",
      "summary": "Provisioning vs configuration management: the real difference between Terraform and Ansible, declarative vs procedural logic, and the hybrid workflow.",
      "content_html": "<p>Terraform builds the servers. Ansible sets up what runs on them. Most pipeline pain comes from asking one to do the other's job.</p>\n\n- Terraform is a declarative provisioning tool: you describe the cloud resources you want and it uses a state file to create, change or destroy them.\n- Ansible is a configuration management tool: agentless playbooks over SSH install and configure software on servers that already exist.\n- The strongest setup uses both: Terraform for the cloud fabric, Ansible for the software layer, wired into one CI/CD pipeline.\n\n<p>You spend six hours clicking through the AWS console to set up a VPC, three subnets, and a load balancer. Then you realize you missed a single security group rule that breaks the entire staging environment.</p>\n<p>Then you have to do it all over again for production. This manual toil is a ticking time bomb for technical debt and human error.</p>\n<p>Infrastructure as Code (IaC) is the industry standard solution, but the market is split between two titans: Terraform and Ansible. Choosing the wrong one for the task leads to fragile pipelines and \"snowflake\" servers that no one dares to touch.</p>\n<h2>Terraform vs Ansible: provisioning vs configuration</h2>\n<p>The most common misunderstanding in DevOps is treating Terraform and Ansible as interchangeable tools. They are not. At their core, they solve two distinct stages of the infrastructure lifecycle.</p>\n<h3>Terraform provisions</h3>\n<p>Terraform is a <strong>provisioning tool</strong>. Its main job is to talk to cloud providers like AWS, Google Cloud, or Azure to create the virtualized \"hardware\" of your stack: VPCs, subnets, IAM roles, and managed databases.</p>\n<p>It builds the foundation your software will live on.</p>\n<h3>Ansible configures</h3>\n<p>Ansible is a <strong>configuration management</strong> tool. Once the server exists, Ansible takes over to install Nginx, configure the PHP-FPM pool, or deploy your latest Laravel build.</p>\n<p>It is designed to manage the internal state of the operating system and the applications running on it.</p>\n<figure>\n<table>\n<thead>\n<tr>\n<th>Feature</th>\n<th>Terraform</th>\n<th>Ansible</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Primary Goal</strong></td>\n<td>Provisioning infrastructure</td>\n<td>Configuration management</td>\n</tr>\n<tr>\n<td><strong>Language</strong></td>\n<td>HCL (declarative)</td>\n<td>YAML (procedural/imperative)</td>\n</tr>\n<tr>\n<td><strong>State</strong></td>\n<td>Managed (state file)</td>\n<td>Stateless</td>\n</tr>\n<tr>\n<td><strong>Architecture</strong></td>\n<td>Client-only (API-based)</td>\n<td>Agentless (SSH/WinRM)</td>\n</tr>\n<tr>\n<td><strong>Best For</strong></td>\n<td>VPCs, clusters, databases</td>\n<td>Installing apps, OS hardening</td>\n</tr>\n</tbody>\n</table>\n\nList every manual step in your last environment setup. Mark each one \"create a cloud resource\" (Terraform) or \"change something inside a server\" (Ansible). That split is your starting automation plan.\n\n<h2>Declarative vs procedural logic</h2>\n<p>The way you talk to these tools defines how you manage your stack. Terraform is strictly <strong>declarative</strong>. You describe the \"end state\" you want.</p>\n<p>If you tell Terraform you want five EC2 instances, it compares that with the instances it already manages. If it manages three, it adds two. If it manages seven, it destroys two.</p>\n<p>You don't tell it <em>how</em> to do it; you tell it what you want the result to be. Instances created by hand outside Terraform are invisible to it until you import them.</p>\n<h3>Ansible runs steps in order</h3>\n<p>Ansible is mainly <strong>procedural</strong> (or imperative), though it uses declarative modules. You write playbooks that list a series of steps:</p>\n<ol>\n<li>Update apt-get.</li>\n<li>Install Docker.</li>\n<li>Copy the config file.</li>\n<li>Restart the service.</li>\n</ol>\n<p>Ansible modules are idempotent (they won't re-install Docker if it's already there), but the workflow follows a specific sequence of commands. This makes Ansible very good at complex application deployments where the order of operations matters.</p>\n\n  <div>\"I want five instances.\" Terraform works out what to add or remove to get there.</div>\n  <div>\"Update, install Docker, copy the config, restart.\" Ansible runs the steps in that order.</div>\n\n<h2>Immutable vs mutable infrastructure</h2>\n<p>The choice between Terraform vs Ansible often shapes your architectural philosophy.</p>\n<h3>Terraform leans immutable</h3>\n<p>Terraform leans heavily toward <strong>immutable infrastructure</strong>. In an immutable world, you don't \"fix\" a server. If you need to update the base OS image, you destroy the old server and provision a new one from a fresh image.</p>\n<p>This is the same philosophy behind <a href=\"https://ansezz.com/blog/docker-vs-kubernetes/\">containers and orchestration</a>. It removes \"configuration drift,\" where servers that started identical become different over time because of manual patches.</p>\n<h3>Ansible is the king of mutable</h3>\n<p>Ansible is the king of <strong>mutable infrastructure</strong>. It is designed to go into existing, long-lived servers and modify them.</p>\n<p>This is often necessary for legacy systems or complex environments where spinning up a fresh cluster for every minor config change is too slow or expensive.</p>\n<p>For teams using self-hosted solutions like <a href=\"https://ansezz.com/blog/coolify-docker-saas-hosting/\">Coolify for SaaS hosting</a>, Ansible is a good fit for managing the underlying VPS environment.</p>\n\nPick one server you patch by hand today. Write the patch as a small Ansible playbook and run it with `--check --diff` first, so you see what would change before it changes.\n\n<h2>State management: the source of truth</h2>\n<p>Terraform's superpower (and its greatest complexity) is the <strong>state file</strong>. This JSON file is a map of your real-world infrastructure.</p>\n<p>When you run a command, Terraform compares your HCL code against this state file and the real resources to work out what needs to change.</p>\n<h3>Drift detection</h3>\n<p>This lets Terraform detect \"drift\": when someone manually changes a setting in the AWS console, Terraform sees the difference and offers to revert it.</p>\n<h3>Ansible keeps no record</h3>\n<p>Ansible is <strong>stateless</strong>. It doesn't keep a record of what it did yesterday. It simply connects to the IP addresses in your inventory and runs the playbook.</p>\n<p>If a server is down, Ansible reports a failure, but it has no \"global view\" of your infrastructure the way Terraform does. This makes Ansible easier to start with but harder to use for tracking the full lifecycle of high-level cloud resources.</p>\n<figure>\n\nRun `terraform plan -refresh-only` against production. Anything it reports is drift someone introduced by hand, and a good first list of what to bring back under code.\n\n<h2>Better together: the hybrid workflow</h2>\n<p>In a professional <a href=\"https://ansezz.com/blog/category/devops/\">DevOps</a> pipeline, you rarely choose just one. Most strong teams use both.</p>\n<h3>A typical workflow</h3>\n<ol>\n<li><strong>Terraform</strong> provisions the network (VPC), the security groups, and the base EC2 instances using a clean Ubuntu AMI.</li>\n<li><strong>Terraform</strong> outputs the IP addresses of those new instances.</li>\n<li><strong>Ansible</strong> picks up those IPs and runs a playbook to install the LEMP stack, configure SSL certificates, and tune the firewall.</li>\n</ol>\n<h3>Why the split works</h3>\n<p>By separating the <strong>cloud fabric</strong> (Terraform) from the <strong>software layer</strong> (Ansible), you create a modular system that is easy to debug and scale.</p>\n<p>If you move from AWS to Google Cloud, you rewrite your Terraform code for the new provider's resources but keep your Ansible playbooks largely the same.</p>\n<p>Wiring both stages into a single <a href=\"https://ansezz.com/blog/ci-vs-cd/\">CI/CD</a> pipeline turns this into a one-command, reproducible build.</p>\n<h3>The licensing caveat</h3>\n<p>One 2026 caveat worth flagging: HashiCorp moved Terraform to the Business Source License in August 2023, and the community forked it into <a href=\"https://opentofu.org/\">OpenTofu</a> (now a CNCF project under the Linux Foundation).</p>\n<p>OpenTofu is a near drop-in replacement using the same HCL syntax and state format, so \"Terraform\" in this article applies equally to it. IBM completed its acquisition of HashiCorp in February 2025, which has only sharpened that licensing question for teams.</p>\n\nAdd a `terraform output -json` step to your pipeline and feed it into an Ansible dynamic inventory, so new servers get configured without anyone copying IPs.\n\n\n- **Terraform** is for building the house (infrastructure). It is declarative, immutable, and stateful.\n- **Ansible** is for decorating and maintaining the house (software configuration). It is procedural, mutable, and stateless.\n- Use **Terraform** to manage resources with a clear lifecycle, like databases and load balancers.\n- Use **Ansible** to manage the \"inside\" of a VM, such as package updates and application deployments.\n- Avoid using Ansible to provision cloud resources; while possible, it lacks the state management and dependency graphing of Terraform.\n- Combine both tools in a CI/CD pipeline for a fully automated, reproducible environment.\n\n<p>If you had to rebuild your entire production environment from scratch in 30 minutes, would your current automation tools be enough to restore both the infrastructure and the application state?</p>\n</figure></figure>",
      "date_published": "2026-06-17T00:00:00.000Z",
      "date_modified": "2026-06-17T00:00:00.000Z",
      "tags": [
        "devops",
        "devops",
        "infrastructure"
      ],
      "image": "https://ansezz.com/blog/terraform-vs-ansible/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/sre-vs-platform-engineer/",
      "url": "https://ansezz.com/blog/sre-vs-platform-engineer/",
      "title": "SRE vs Platform Engineer: who to hire for scale",
      "summary": "SRE vs Platform Engineer compared: reliability versus developer experience, what each role owns, and which one your team needs to hire first.",
      "content_html": "<p>An SRE keeps production up for your users. A platform engineer makes shipping easy for your developers. Hire for the bottleneck you have today.</p>\n\n- SREs own production reliability: SLIs, SLOs, error budgets, on-call, incident response and automating away toil.\n- Platform engineers own developer experience: an internal developer platform with self-service \"Golden Paths\" from laptop to production.\n- The two feed each other: SREs find recurring failures, platform engineers bake the fix into the defaults so every new service gets it.\n\n<p>Your production environment is down and the error budget is burning. You have two choices.</p>\n<p>You can hire someone to fix the immediate fire and make sure it never happens again. Or you can hire someone to build the tools that keep your developers from starting that fire in the first place.</p>\n<p>This is the core tension between Site Reliability Engineering (SRE) and Platform Engineering.</p>\n<p>For years, \"DevOps\" was the catch-all term for anyone who touched a server. As systems grew more complex, the roles split into specialized disciplines. SRE and Platform Engineering are the two most visible results.</p>\n<p>They often share tools like Kubernetes, Terraform, and Docker, but their missions are worlds apart. One focuses on the runtime reality of the user. The other focuses on the internal experience of the developer.</p>\n<h2>The SRE: the guardian of production</h2>\n<p>Site Reliability Engineering is what happens when you ask a software engineer to design an operations function. Originally pioneered by Google, SRE treats operations as a software problem.</p>\n<p>The mission of an SRE is simple but difficult: make sure production services are reliable, scalable, and performant.</p>\n<h3>SLOs and error budgets</h3>\n<p>SREs live and breathe metrics. They define Service Level Indicators (SLIs) and Service Level Objectives (SLOs) to put a number on what \"up\" actually means.</p>\n<p>If a service has a 99.9% availability target, the SRE manages the \"error budget.\" This is the remaining 0.1% of downtime allowed before the team must stop shipping features and focus on stability.</p>\n<h3>Killing toil</h3>\n<p>The daily life of an SRE involves on-call rotations, incident response, and post-mortem analysis. Beyond fixing bugs, they build automation to eliminate \"toil.\"</p>\n<p>Toil is the manual, repetitive work, like scaling clusters or rotating certificates, that grows linearly with the size of the system.</p>\n<p>By writing code to automate these tasks, an SRE lets the infrastructure grow without a matching increase in headcount.</p>\n<figure>\n\nPick your most important user-facing endpoint and write one SLO for it, for example \"99.9% of requests succeed in under 300ms over 30 days.\" Then work out how many minutes of failure that leaves you each month.\n\n<h2>The Platform Engineer: architect of the golden path</h2>\n<p>Platform Engineering is the practice of building and running internal developer platforms (IDPs). If the SRE focuses on the end user, the Platform Engineer focuses on the internal developer.</p>\n<p>Their goal is to improve Developer Experience (DX) by hiding the complexity of the underlying infrastructure.</p>\n<h3>Golden Paths</h3>\n<p>A Platform Engineer builds \"Golden Paths.\" These are standardized, self-service workflows that let a developer go from \"code on my laptop\" to \"running in production\" without opening a ticket for the infrastructure team.</p>\n<p>Instead of every developer learning the details of AWS IAM roles or Kubernetes ingress controllers, they use a platform that handles these details automatically.</p>\n<h3>The platform is a product</h3>\n<p>Platform Engineers are measured by lead time to production and developer satisfaction. They treat the platform as a product and run user research with their internal developers to find friction points.</p>\n<p>They build CI/CD pipelines, environment provisioning tools, and internal portals that make shipping software feel effortless. You can see similar patterns in how tools like <a href=\"https://ansezz.com/blog/coolify-docker-saas-hosting/\">Coolify simplify SaaS hosting</a> by providing one management layer.</p>\n<h2>The core divergence: metrics and customers</h2>\n<p>The easiest way to tell these roles apart is to look at who they serve and how they are measured.</p>\n<table>\n<thead>\n<tr>\n<th>Feature</th>\n<th>SRE</th>\n<th>Platform Engineer</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Primary Customer</strong></td>\n<td>End users &amp; product teams</td>\n<td>Internal developers</td>\n</tr>\n<tr>\n<td><strong>Success Metric</strong></td>\n<td>SLOs, latency, MTTR</td>\n<td>Lead time, deployment frequency</td>\n</tr>\n<tr>\n<td><strong>Key Focus</strong></td>\n<td>Production reliability</td>\n<td>Developer productivity</td>\n</tr>\n<tr>\n<td><strong>Artifacts</strong></td>\n<td>Alerting rules, runbooks, SLOs</td>\n<td>IDPs, CLI tools, CI/CD templates</td>\n</tr>\n</tbody>\n</table>\n<h3>The \"now\" and the \"how\"</h3>\n<p>An SRE cares about the \"now.\" Is the site up? Is the latency within limits? If a database query takes 500ms instead of the usual 50ms, the SRE is the first person to notice. They are the front line for the business's reputation.</p>\n<p>A Platform Engineer cares about the \"how.\" How long does it take a new hire to ship their first line of code? How many manual steps are in the deployment process?</p>\n<p>They are the industrial engineers of the software factory. They optimize the assembly line so everyone else can move faster.</p>\n<p>This role matters more as we move toward <a href=\"https://ansezz.com/blog/agentic-commerce-shopify/\">agentic commerce and automated Shopify workflows</a>, where the speed of iteration is a competitive advantage.</p>\n<h2>A Formula 1 metaphor</h2>\n<p>Imagine a Formula 1 team.</p>\n\n  <div>Watches the car in real time during the race: tire pressure, fuel, engine temperature. If something goes wrong on lap 30, they make the split-second call that keeps the car on the track.</div>\n  <div>Designed the car and the tools used to build it: the wind tunnel, the simulation software, the standardized parts. They make sure the driver and pit crew have the best equipment for the job.</div>\n\n<p>Without the factory engineer, the car wouldn't be fast. Without the race engineer, the car wouldn't finish the race.</p>\n<h2>The tools of the trade</h2>\n<p>Both roles use similar technologies, but they apply them differently.</p>\n<p><strong>SRE tooling:</strong></p>\n<ul>\n<li><strong>Observability:</strong> Prometheus, Grafana, Datadog, Honeycomb.</li>\n<li><strong>Incident management:</strong> PagerDuty, Jira Service Management (Atlassian stopped selling Opsgenie in June 2025 and shuts it down in April 2027).</li>\n<li><strong>Resilience:</strong> Chaos Mesh, Gremlin.</li>\n<li><strong>Automation:</strong> Python, Go, Ansible.</li>\n</ul>\n<p><strong>Platform engineering tooling:</strong></p>\n<ul>\n<li><strong>Internal portals:</strong> Backstage, Port, Cortex.</li>\n<li><strong>Infrastructure as Code:</strong> Terraform, Pulumi, Crossplane.</li>\n<li><strong>CI/CD:</strong> GitHub Actions, GitLab CI, ArgoCD.</li>\n<li><strong>Developer environments:</strong> Loft, DevPod, Docker.</li>\n</ul>\n<h3>Same tools, different jobs</h3>\n<p>The SRE uses observability tools to find \"unknown unknowns\" in production. The Platform Engineer uses those same tools to bake monitoring defaults into the platform.</p>\n<p>If you want to dig deeper into the technical stack for modern applications, check our guide on the <a href=\"https://ansezz.com/blog/api-gateway-ai-stack/\">API gateway for the AI stack</a>.</p>\n<h2>Better together: the feedback loop</h2>\n<p>In a high-performing organization, these two roles form a tight feedback loop.</p>\n<p>The SRE discovers a recurring failure mode during an incident. Perhaps developers forget to set proper resource limits on their containers, and nodes run out of memory.</p>\n<h3>From incident to default</h3>\n<p>Instead of telling developers to \"be more careful,\" the SRE brings this feedback to the Platform Engineer. The Platform Engineer updates the \"Golden Path\" templates to include sensible default resource limits automatically.</p>\n<p>Now every new service created on the platform is \"reliable by design.\"</p>\n<h3>Everyone wins</h3>\n<p>This loop reduces the SRE's operational load. They spend less time fighting fires and more time on high-level architecture.</p>\n<p>At the same time, developers are happier because the platform keeps them from making common mistakes. This is what <a href=\"https://ansezz.com/blog/category/devops/\">mature DevOps</a> looks like.</p>\n<figure>\n\nTake your last three post-mortems. For each one, ask: \"Could a default in our templates or pipeline have prevented this?\" Every yes is a ticket for whoever owns your platform.\n\n<h2>Which one do you need?</h2>\n<p>The choice depends on your current pain points.</p>\n<h3>Hire a Platform Engineer if...</h3>\n<p>Your developers keep struggling to set up environments, fighting CI/CD pipelines, or waiting weeks for infrastructure tickets. You have a productivity bottleneck. You need to productize your infrastructure so your team can scale.</p>\n<h3>Hire an SRE if...</h3>\n<p>Your site is frequently down, your performance is unpredictable, and your developers are terrified of shipping on a Friday. You have a reliability bottleneck. You need someone to bring the discipline of SLOs and incident management into your culture.</p>\n<h3>The split comes with growth</h3>\n<p>In many startups, a single \"DevOps Engineer\" handles both. As you grow, the split is inevitable, because specialized roles allow deeper expertise.</p>\n<p>An SRE who doesn't have to build CI/CD pipelines can focus on making the system hard to break. A Platform Engineer who isn't on call for every production blip can focus on making the developer experience excellent.</p>\n\nFor one week, tag every infrastructure complaint from your team as \"it broke\" or \"it's slow to ship.\" Whichever pile is bigger tells you which role to hire first.\n\n\n- SRE focuses on the reliability of production systems for end users.\n- Platform Engineering focuses on the developer experience and internal tooling.\n- SRE uses SLOs and error budgets to balance speed and stability.\n- Platform Engineering uses \"Golden Paths\" to provide self-service infrastructure.\n- The two roles are complementary. SRE provides the feedback, and Platform Engineering provides the abstractions.\n- Both roles are essential for scaling modern, complex software organizations.\n\n<p>How does your team handle the balance between shipping fast and staying up?</p>\n</figure></figure>",
      "date_published": "2026-06-16T00:00:00.000Z",
      "date_modified": "2026-06-16T00:00:00.000Z",
      "tags": [
        "career",
        "devops",
        "infrastructure",
        "kubernetes",
        "observability",
        "career"
      ],
      "image": "https://ansezz.com/blog/sre-vs-platform-engineer/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/synchronous-vs-asynchronous-communication/",
      "url": "https://ansezz.com/blog/synchronous-vs-asynchronous-communication/",
      "title": "Synchronous vs asynchronous communication",
      "summary": "The technical differences between synchronous and asynchronous architectures in Laravel and Shopify, and how queues, jobs, and webhooks help you scale.",
      "content_html": "<p>A slow API call inside a web request ties up a worker that every other user is waiting for.</p>\n\n- Synchronous calls block: the worker waits until the task finishes. Fine for fast reads and writes, dangerous for slow third-party APIs.\n- Asynchronous work goes on a queue and runs in a background worker, so the user gets an instant response and failed jobs can retry.\n- In Laravel and Shopify that means: dispatch jobs for slow work, and answer webhooks fast, then process them in a job.\n\n<p>Every developer has faced the dreaded \"spinning wheel of death.\" You click a button, the browser hangs, and you wait five seconds for a confirmation that never feels fast enough.</p>\n<p>This lag usually comes from a synchronous process holding the entire request-response cycle hostage while it waits for a third-party API or a heavy database query.</p>\n<p>In high-traffic environments, these blocking calls are more than a nuisance. They are a scalability killer that can bring down your entire infrastructure.</p>\n<p>Architecting for scale means knowing when to keep things synchronous and when to move them to the background. This decision affects everything from user experience to server costs.</p>\n<p>By shifting heavy lifting to asynchronous workers, you decouple your system. This lets your application handle thousands of concurrent users without breaking a sweat.</p>\n<h2>The blocking nature of synchronous communication</h2>\n<p>Synchronous communication is the traditional request-response model. When a client sends a request, it waits for the server to process the logic and return a response.</p>\n<p>This is a \"blocking\" operation. The execution thread is tied up until the task completes.</p>\n<h3>Like a phone call</h3>\n<p>Think of it like a phone call. You dial a number, wait for the other person to pick up, and you cannot do anything else until the conversation is over.</p>\n<p>In web development, this is perfectly fine for simple operations like fetching a user profile or updating a single database row. The latency is minimal and the user needs the data immediately to continue.</p>\n<h3>When sync becomes dangerous</h3>\n<p>Synchronous flows become dangerous when you add external dependencies. If your Laravel controller calls the Shopify Admin API to update 500 products during a standard HTTP request, your PHP worker is stuck.</p>\n<p>If Shopify takes three seconds to respond, that worker cannot serve any other users for those three seconds.</p>\n<p>Under heavy load, your worker pool will run out. This leads to 504 Gateway Timeout errors and a broken experience.</p>\n\nFind your three slowest controller actions in your APM or logs. For each one, note whether the time is spent waiting on an external API. Those are your first candidates for a queue.\n\n<h2>The fire-and-forget power of asynchronous logic</h2>\n<p>Asynchronous communication decouples the request from the processing. The client sends a message or triggers an event and immediately receives a response.</p>\n<p>The actual work happens \"out of band\" in a separate process or at a later time.</p>\n<h3>Like sending an email</h3>\n<p>This is more like sending an email. You hit send and go straight back to your day. The recipient reads and processes the message whenever they are available.</p>\n<p>In a technical stack, this is done with message queues like Redis or Amazon SQS, or a dedicated broker such as <a href=\"https://ansezz.com/blog/scaling-with-rabbitmq/\">RabbitMQ</a>.</p>\n\n  <div>A phone call. The worker waits on the line until the task is done, and nobody else can use it.</div>\n  <div>An email. The job goes on a queue, the user gets an answer right away, and a background worker does the rest.</div>\n\n<figure>\n<h3>What belongs in the background</h3>\n<p>When you move a task to an asynchronous worker, the user gets an instant \"Success\" message. The heavy processing happens in the background. This architecture is essential for:</p>\n<ul>\n<li>Sending transactional emails.</li>\n<li>Generating PDF reports or data exports.</li>\n<li>Communicating with third-party APIs like Shopify or Stripe.</li>\n<li>Running complex AI agents or RAG pipelines.</li>\n</ul>\n<h2>Laravel queues and background jobs</h2>\n<p>Laravel gives you asynchronous processing through its Queue system. Instead of performing a slow task in your controller, you dispatch a \"Job.\"</p>\n<pre><code>// Synchronous (Bad for slow tasks)\npublic function store(Request $request) {\n    $order = Order::create($request-&gt;all());\n    Mail::to($request-&gt;user())-&gt;send(new OrderConfirmed($order)); // Waits for SMTP\n    return response()-&gt;json($order);\n}\n\n// Asynchronous (Scalable)\npublic function store(Request $request) {\n    $order = Order::create($request-&gt;all());\n    ProcessOrderEmail::dispatch($order); // Returns immediately\n    return response()-&gt;json($order);\n}\n</code></pre>\n<h3>Workers and retries</h3>\n<p>By using <code>dispatch()</code>, you push a payload into Redis. A separate process, managed by a tool like <a href=\"https://laravel.com/docs/horizon\">Laravel Horizon</a>, picks up the job and runs it. This keeps your web-facing workers free to handle more incoming traffic.</p>\n<p>If the email server is down and the job has retries configured (<code>$tries</code> and <code>$backoff</code> on the job, or <code>--tries</code> on the worker), the job goes back on the queue and tries again later.</p>\n<p>This kind of resilience is impossible in a strictly synchronous world.</p>\n\nOpen one of your job classes and set `public $tries = 5;` and `public $backoff = [10, 60, 300];`. Laravel's default is a single attempt, so without this a failed job just fails.\n\n<h2>Shopify webhooks: the event-driven standard</h2>\n<p>For e-commerce developers, understanding asynchronous communication is mandatory when working with Shopify. Shopify uses webhooks to notify your application of events like <code>orders/create</code> or <code>products/update</code>.</p>\n<h3>Five seconds to answer</h3>\n<p>Webhooks are asynchronous by design. Shopify does not wait for your app to process the entire order. It sends the POST request and expects a <code>2xx</code> response within five seconds.</p>\n<p>If your code tries to run complex inventory logic or sync data to an ERP synchronously within that webhook route, you risk timing out.</p>\n<h3>Retries and removal</h3>\n<p>Shopify then retries the failed delivery up to eight times over roughly four hours, which can mean duplicate processing if your handler is not idempotent.</p>\n<p>Worse, if your endpoint keeps failing, Shopify removes the webhook subscription entirely, and you stop receiving events until you re-register it.</p>\n<p>Receive the webhook, validate the signature, dispatch a Laravel Job, and return the response immediately.</p>\n<p>This is the best practice for <a href=\"https://ansezz.com/blog/agentic-commerce-shopify/\">agentic commerce on Shopify</a>. It keeps your app responsive and keeps Shopify happy with your delivery rates.</p>\n<figure>\n\nTime your webhook route end to end. If anything after the HMAC check takes more than a few hundred milliseconds, move it into a job and return `200` first.\n\n<h2>Infrastructure impact and DevOps considerations</h2>\n<p>Moving to an asynchronous architecture changes how you manage your infrastructure. In a synchronous world, you scale your web servers to handle peak traffic. In an asynchronous world, you scale your workers.</p>\n<h3>Separate web and worker containers</h3>\n<p>Tools like <a href=\"https://ansezz.com/blog/coolify-docker-saas-hosting/\">Coolify and Docker</a> make it easier to manage these separate services. You can run your web container on one set of resources and your worker containers on another.</p>\n<p>If you get a massive spike in background jobs, you can spin up more workers without affecting the performance of your main website.</p>\n<h3>Managed queues</h3>\n<p>For cross-service communication, Google Cloud Tasks is a strong alternative to a self-hosted queue. It dispatches HTTP callbacks to any endpoint with built-in rate limiting, scheduling, and retries.</p>\n<p>This is particularly useful when building an <a href=\"https://ansezz.com/blog/api-gateway-ai-stack/\">API gateway for AI stacks</a>, where different microservices need to talk to each other without blocking the main user flow.</p>\n<h2>When to choose sync vs async</h2>\n<p>Choosing the right pattern depends on the user's expectations and the reliability of the task.</p>\n<table>\n<thead>\n<tr>\n<th>Feature</th>\n<th>Synchronous</th>\n<th>Asynchronous</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>User Experience</strong></td>\n<td>Immediate feedback required.</td>\n<td>Background processing is okay.</td>\n</tr>\n<tr>\n<td><strong>Data Integrity</strong></td>\n<td>Result needed for next step.</td>\n<td>Eventual consistency is okay.</td>\n</tr>\n<tr>\n<td><strong>Reliability</strong></td>\n<td>Fails if the connection breaks.</td>\n<td>Can retry on failure (set $tries).</td>\n</tr>\n<tr>\n<td><strong>Complexity</strong></td>\n<td>Simple code structure.</td>\n<td>Requires queue management and workers.</td>\n</tr>\n<tr>\n<td><strong>Example Use Case</strong></td>\n<td>Login validation, viewing a cart.</td>\n<td>Order fulfillment, image processing.</td>\n</tr>\n</tbody>\n</table>\n<h3>Use synchronous when:</h3>\n<ul>\n<li>The operation is extremely fast (under 100ms).</li>\n<li>The user cannot proceed without the result of the operation.</li>\n<li>You are performing a simple read/write to your primary database.</li>\n</ul>\n<h3>Use asynchronous when:</h3>\n<ul>\n<li>You are calling an external API.</li>\n<li>The task takes more than 200ms to complete.</li>\n<li>The task can fail and needs to be retried.</li>\n<li>You are performing bulk operations on large datasets.</li>\n</ul>\n\nDesigning for high performance means not making every user wait on every task. By embracing asynchronous communication, you build systems that are more reliable and easier to scale.\n<ol>\n<li><strong>Identify bottlenecks</strong>: Use tools like Laravel Telescope or OpenTelemetry to find slow controller actions.</li>\n<li><strong>Offload everything</strong>: If it doesn't need to happen <em>right now</em>, push it to a queue.</li>\n<li><strong>Handle webhooks fast</strong>: Always return a 200 response to Shopify immediately and process the logic later.</li>\n<li><strong>Monitor your workers</strong>: Use dashboards like Laravel Horizon to track queue health and failure rates.</li>\n<li><strong>Design for retries</strong>: Ensure your background jobs are idempotent, meaning they can run multiple times without causing side effects.\n</li></ol>\n\n<p>How are you currently handling long-running API tasks in your Laravel or Shopify application?</p>\n</figure></figure>",
      "date_published": "2026-06-16T00:00:00.000Z",
      "date_modified": "2026-06-16T00:00:00.000Z",
      "tags": [
        "architecture",
        "laravel",
        "shopify",
        "devops",
        "architecture",
        "messaging"
      ],
      "image": "https://ansezz.com/blog/synchronous-vs-asynchronous-communication/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/solutions-architect-vs-forward-deployed-engineer/",
      "url": "https://ansezz.com/blog/solutions-architect-vs-forward-deployed-engineer/",
      "title": "Solutions architect vs forward deployed engineer",
      "summary": "Solutions architect vs forward deployed engineer: one owns the blueprint, the other owns the outcome. Who carries delivery risk, and which you need.",
      "content_html": "<p>A solutions architect owns the blueprint. A forward deployed engineer owns the outcome. When a project fails, it usually fails in the gap between them.</p>\n\n- Solutions architects design: they turn business requirements into a low-risk architecture, with diagrams and design docs, then hand off to delivery.\n- Forward deployed engineers (FDEs) build: they work inside the customer's environment, write the production code and own whether it works.\n- Pick by product: integration-heavy but repeatable products need architects; complex platforms that must be adapted to each customer's data need FDEs.\n\n<p>Writing code is only part of the job. Where that code sits in the customer's value chain matters just as much, and the difference between a solutions architect and a forward deployed engineer decides who is accountable when it lands there.</p>\n<p>Many organizations invest millions in complex software platforms like Shopify Plus or custom Laravel ecosystems, only to realize they cannot bridge the gap between a sales demo and a running production system.</p>\n<p>The implementation phase becomes a graveyard of missed requirements and technical debt. Sales cycles stall because the \"how\" is missing. This is exactly where the two roles diverge.</p>\n<p>Both roles sit where engineering meets customer success, but they solve different problems. One builds the blueprint so the house will stand. The other moves onto the construction site to make sure the pipes actually connect to the city main.</p>\n<h2>The design blueprint: defining the Solutions Architect</h2>\n<p>The Solutions Architect is the strategic engine of a technical engagement. Their main goal is to design a scalable, low-risk implementation architecture that maps customer business requirements to technical capabilities.</p>\n<p>They operate in the space between a \"yes\" from the buyer and a \"go-live\" from the developers.</p>\n<h3>Working at the macro level</h3>\n<p>An architect focuses on the big picture. They evaluate how a new <a href=\"https://ansezz.com/blog/category/laravel/\">Laravel development</a> project will integrate with existing legacy databases, or how a <a href=\"https://ansezz.com/blog/category/shopify/\">Shopify app</a> will interact with a third-party ERP.</p>\n<p>Their deliverables are not usually production pull requests. Instead, they produce sequence diagrams, entity-relationship diagrams (ERDs), and security compliance documents.</p>\n<h3>Owning the design</h3>\n<p>They spend their days in design workshops and advisory conversations. They must convince a CTO that the proposed cloud infrastructure is secure, while proving to the marketing manager that the data flow will support their holiday campaigns.</p>\n<p>The Solutions Architect owns the design. They make sure that if the plan is followed, the system will be stable, secure, and performant.</p>\n<p>Once the blueprint is approved, though, they often move on to the next challenge and hand the execution to a delivery team.</p>\n<figure>\n<h2>The boots on the ground: defining the Forward Deployed Engineer</h2>\n<p>The Forward Deployed Engineer is a hybrid role born in high-stakes enterprise software. Pioneered by Palantir to embed engineers in messy, sensitive customer data environments, the FDE is a full-stack engineer who works directly inside the customer's environment.</p>\n<p>They write the production code that makes the system real.</p>\n<h3>Engineer and diplomat</h3>\n<p>If a Solutions Architect is a designer, the FDE is a lead engineer who is also a specialized diplomat. They inherit the \"messy\" reality of customer data, undocumented APIs, and internal politics.</p>\n<p>When a <a href=\"https://ansezz.com/blog/7-rag-mistakes-production/\">RAG pipeline</a> fails because the customer's vector database has inconsistent metadata, the FDE is the one debugging the ingestion script at 2:00 AM.</p>\n<h3>Owning the outcome</h3>\n<p>The FDE owns the outcome. They are measured by whether the system actually works in production and delivers business value.</p>\n<p>This role needs a high tolerance for ambiguity and the ability to pivot when a design meets the harsh reality of production.</p>\n<p>They are also the company's best feedback loop. They find the bugs and missing features in the core product and push those requirements back to the internal product teams.</p>\n<h2>Core differences: strategic design vs production ownership</h2>\n<p>The divide between these roles comes down to where they sit in the software development lifecycle (SDLC) and who owns the risk.</p>\n<table>\n<thead>\n<tr>\n<th>Feature</th>\n<th>Solutions Architect</th>\n<th>Forward Deployed Engineer</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Primary output</strong></td>\n<td>Blueprints, PoCs, design docs</td>\n<td>Production code, integrations, live systems</td>\n</tr>\n<tr>\n<td><strong>Lifecycle stage</strong></td>\n<td>Pre-sales and discovery</td>\n<td>Implementation and post-launch</td>\n</tr>\n<tr>\n<td><strong>Code ownership</strong></td>\n<td>Reference implementations only</td>\n<td>Direct commits to customer repos</td>\n</tr>\n<tr>\n<td><strong>Main stakeholder</strong></td>\n<td>CTOs and architects</td>\n<td>Engineering managers and devs</td>\n</tr>\n<tr>\n<td><strong>Goal</strong></td>\n<td>Risk reduction and scalability</td>\n<td>Successful deployment and value delivery</td>\n</tr>\n</tbody>\n</table>\n<h3>One design, two jobs</h3>\n<p>A Solutions Architect might design a multi-region deployment strategy on Google Cloud Platform (GCP) for a global e-commerce brand. They specify the load balancers, the database replication strategy, and the caching layers.</p>\n<p>A Forward Deployed Engineer then takes that design and writes the Terraform scripts, configures the Docker containers, and debugs the latency issues that appear when the first 10,000 users hit the site.</p>\n<p>The architect operates at a level of abstraction. The FDE operates at a level of execution.</p>\n<p>This is why FDEs are often found in companies selling complex, horizontal platforms that need significant customization to be useful.</p>\n\nLook at your last delayed project and find the moment the design doc was \"done.\" List every problem that showed up after that point. If most of them needed code, not a better diagram, you were missing an FDE.\n\n<h2>Real-world scenarios: from Laravel to AI agents</h2>\n<p>To see how these roles interact, picture the deployment of an agentic AI system for a large retailer. The stack is complex: a Laravel backend, a Shopify frontend, and an AI orchestration layer using Claude and MCP.</p>\n<h3>The architect goes first</h3>\n<p>The Solutions Architect starts the project. They define how the <a href=\"https://ansezz.com/blog/claude-mcp-dev-tools/\">Claude MCP dev tools</a> will interface with the retailer's private product catalog.</p>\n<p>They map out the authentication flow so the AI agent cannot access sensitive customer financial data. They produce a high-level technical document that the retailer's security team signs off on.</p>\n<h3>The FDE moves in</h3>\n<p>Once the \"paperwork\" is done, the Forward Deployed Engineer moves in. They realize the product catalog API is much slower than promised, so they write a custom Redis caching layer in the Laravel middleware.</p>\n<p>They spend three days pairing with the retailer's frontend team to integrate the AI chat widget into a bespoke Shopify theme.</p>\n<p>They are also the ones who notice the RAG system needs a different chunking strategy for the type of PDF manuals the retailer uses. They fix it in the code.</p>\n\n  <div>The project was safe and technically sound from a high level, and the security team signed off.</div>\n  <div>The specific, messy details of this one customer did not sink the implementation.</div>\n\n<h2>Hiring the right talent: which one do you need?</h2>\n<p>Choosing between a Solutions Architect and a Forward Deployed Engineer depends on your product's complexity and your business model.</p>\n<h3>When an architect is enough</h3>\n<p>If you are a consultancy or software vendor with a product that is \"plug-and-play\" but needs significant integration planning, a Solutions Architect is your best bet.</p>\n<p>They can handle five or six customers at once, giving the high-level guidance that keeps projects moving without getting stuck in any single code repository.</p>\n<h3>When you need FDEs</h3>\n<p>If you are building \"deep tech\" or complex platforms like AI-driven supply chain tools or custom enterprise ERPs, you need Forward Deployed Engineers.</p>\n<p>These products are never truly \"plug-and-play.\" They need an engineer to sit with the customer for months, adapting the product to the data.</p>\n\nAsk your last three customers how long it took from signed contract to first real value in production. If the answer is months and full of custom code, plan for FDEs in your next hire.\n\n<h3>The candidate's view</h3>\n<p>From a candidate's point of view, the choice comes down to the \"blank page\" versus the \"broken system.\"</p>\n<p>Architects enjoy the creative challenge of designing from scratch and the variety of working across many industries. FDEs enjoy shipping code that solves a tangible, immediate problem for a person they work with every day.</p>\n<p>If you are weighing engineering tracks more broadly, the same blueprint-versus-operations split shows up in <a href=\"https://ansezz.com/blog/sre-vs-platform-engineer/\">SRE vs platform engineer</a> and in <a href=\"https://ansezz.com/blog/cloud-engineer-vs-devops-engineer/\">cloud engineer vs DevOps engineer</a>.</p>\n<figure>\n\n- **Solutions Architects** focus on the \"what\" and \"why\" of a system. They provide the strategic design and reduce technical risk during the early stages of a project.\n- **Forward Deployed Engineers** focus on the \"how.\" They are hands-on coders who live inside the customer's environment to ensure a system actually reaches production.\n- **Ownership** is the main divider. Architects own the design document. FDEs own the live production outcome.\n- **Feedback loops** differ. Architects improve the company's \"standards.\" FDEs improve the company's \"product\" by finding real-world bugs.\n- **Skill sets** overlap but diverge at the keyboard. Both need strong communication skills, but the FDE must be a top-tier debugger and implementation specialist.\n<p>A project rarely fails because of a bad design or a bad implementation alone. It fails in the gap between the two, so the best teams make these roles work in tandem.\n</p><p></p>\n<p>If you need a Forward Deployed Engineer to close that gap, <a href=\"https://ansezz.com/services/\">here's how I help teams ship</a>.</p>\n<p>If you are scaling a technical team right now, are you optimizing for the number of designs approved or the number of systems successfully deployed into production?</p>\n</figure></figure>",
      "date_published": "2026-06-15T00:00:00.000Z",
      "date_modified": "2026-06-15T00:00:00.000Z",
      "tags": [
        "career",
        "architecture",
        "devops",
        "career"
      ],
      "image": "https://ansezz.com/blog/solutions-architect-vs-forward-deployed-engineer/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/rest-vs-grpc/",
      "url": "https://ansezz.com/blog/rest-vs-grpc/",
      "title": "REST vs gRPC: two API philosophies",
      "summary": "The technical differences between REST and gRPC: when to use Protocol Buffers over JSON for high-performance microservices, and why good designs use both.",
      "content_html": "<p>REST is built for humans and browsers. gRPC is built for machines talking to machines. Good architectures use each where it fits.</p>\n\n- gRPC sends compact binary Protocol Buffers over HTTP/2, with a contract-first `.proto` schema, generated stubs and native streaming.\n- REST sends human-readable JSON over plain HTTP that every browser and tool understands, which makes it the default for public APIs.\n- The common pattern: REST at the public edge, gRPC between internal services.\n\n<p>Choosing between REST and gRPC is a choice of architectural philosophy as much as protocol, and it shapes how your services communicate.</p>\n<p>Pick the wrong one for a high-traffic microservice environment and you will spend your time fighting network latency and rising cloud costs as JSON payloads bloat under load.</p>\n<p>Force a binary protocol onto a public-facing web client and you break the interoperability that makes the modern web work.</p>\n<p>The choice between Representational State Transfer (REST) and gRPC (a Google-built RPC framework whose name is a recursive joke: \"gRPC Remote Procedure Calls\") is a trade-off between human readability and machine efficiency.</p>\n<p>REST has been the champion of the web for decades, while gRPC has quickly become the preferred choice for internal service-to-service communication. Understanding both is essential for any engineer building scalable systems in 2026.</p>\n<h2>The mechanical advantage of gRPC</h2>\n<p>At its core, gRPC is built to be fast. It gets there by moving away from the text-based nature of REST and using a binary format called Protocol Buffers (Protobuf).</p>\n<h3>The JSON overhead</h3>\n<p>In a typical REST interaction, your server takes an object, serializes it into a JSON string, and sends it over the wire. The receiving server then parses that string back into an object.</p>\n<p>This process is CPU-intensive and creates large payloads, because JSON includes every key name in every single message.</p>\n<h3>How Protobuf avoids it</h3>\n<p>Protobuf is a strongly typed binary serialization format. Because the schema is defined in advance in a <code>.proto</code> file, the messages do not need to include field names. Instead, they use small numeric tags to identify each field.</p>\n<p>The payload savings depend heavily on data shape. Numeric and structured data shrinks a lot, often to several times smaller than the equivalent JSON, while large free-text fields see less benefit.</p>\n<p>Either way, binary Protobuf parsing is typically much faster than JSON parsing, which cuts serialization CPU in high-throughput environments.</p>\n<h3>HTTP/2 underneath</h3>\n<p>Another pillar of gRPC is HTTP/2. Many REST implementations still run on HTTP/1.1, but gRPC requires HTTP/2, which brings request multiplexing.</p>\n<p>In HTTP/1.1, a client effectively sends one request at a time over a connection, which leads to application-level head-of-line blocking. HTTP/2 multiplexes many requests and responses over a single TCP connection and removes that HTTP-layer blocking.</p>\n<p>(TCP-level head-of-line blocking still exists under packet loss. That is the problem HTTP/3 and QUIC were designed to solve.) This efficiency matters for modern <a href=\"https://ansezz.com/blog/api-gateway-ai-stack/\">API gateway architectures</a> that handle thousands of concurrent requests.</p>\n<figure>\n\nTake one real response from your busiest internal endpoint, write the matching `.proto` message, and compare the encoded size with the JSON size. That ratio, for your data, is the number that matters.\n\n<h2>The universality of REST</h2>\n<p>If gRPC is so much faster, why hasn't it replaced REST? The answer lies in the browser.</p>\n<p>Browsers cannot speak native gRPC directly. gRPC-Web exists, but it needs a proxy in front of your services and does not support every streaming mode.</p>\n<h3>Resources and verbs</h3>\n<p>REST was born from the architecture of the web itself. It treats every piece of data as a resource that can be accessed with standard HTTP verbs like GET, POST, PUT, and DELETE. This resource-oriented approach makes it intuitive and easy to consume.</p>\n<p>Every web browser, every language, and every command-line tool like <code>curl</code> understands REST. If you are building a public API that third-party developers will use, REST is usually the right choice.</p>\n<h3>Inspectability</h3>\n<p>The barrier to entry is almost zero. A developer can open their browser console and immediately see what your API returns in a human-readable format. This \"inspectability\" is a big advantage for debugging and onboarding.</p>\n<h3>Flexibility</h3>\n<p>REST is also flexible. It does not require a strict contract to function. Tools like OpenAPI (Swagger) provide structure, but a REST API can still evolve loosely.</p>\n<p>This flexibility suits startups and small teams that need to iterate quickly. When you are deploying a new SaaS product on <a href=\"https://ansezz.com/blog/coolify-docker-saas-hosting/\">Coolify or Docker</a>, the simplicity of a RESTful interface often outweighs the performance gains of gRPC in the early stages.</p>\n<h2>Schema-first vs resource-first development</h2>\n<p>One of the biggest differences between these two philosophies is how you actually write code. gRPC forces a \"contract-first\" approach.</p>\n<p>You must define your service and your message types in a <code>.proto</code> file before you write a single line of application logic.</p>\n<pre><code>// A typical gRPC service definition\nservice ProductService {\n  rpc GetProduct (ProductRequest) returns (ProductResponse);\n}\n\nmessage ProductRequest {\n  string id = 1;\n}\n\nmessage ProductResponse {\n  string name = 1;\n  double price = 2;\n}\n</code></pre>\n<h3>Generated stubs, shared types</h3>\n<p>Once this file is defined, gRPC tools generate client and server stubs in the language of your choice. This gives you strong typing across services.</p>\n<p>If you have a Go backend talking to a Python microservice, the Protobuf contract ensures both sides agree on the data structure. This removes a whole class of bugs caused by missing fields or incorrect types in JSON.</p>\n<h3>The risk of API drift</h3>\n<p>REST typically follows a \"resource-first\" or \"implementation-first\" approach. You define your routes and your controllers, and then you might generate documentation later.</p>\n<p>This allows faster prototyping, but it can lead to \"API drift,\" where the documentation and the actual implementation fall out of sync. In a large <a href=\"https://ansezz.com/blog/monolith-vs-microservices/\">microservice ecosystem</a>, this lack of strict contracts can become a maintenance nightmare.</p>\n\n  <div>Write routes and controllers, document later. Fast to prototype, easy to drift out of sync.</div>\n  <div>Write the `.proto` contract first, generate typed stubs for every language. Slower to start, hard to drift.</div>\n\n\nIf you keep REST, generate an OpenAPI spec from your code in CI and fail the build when it changes without a matching docs update. You get some of gRPC's contract safety without switching protocols.\n\n<h2>Streaming and real-time capabilities</h2>\n<p>Another area where gRPC shines is native streaming. Because it is built on HTTP/2, gRPC supports four types of communication:</p>\n<ul>\n<li><strong>Unary:</strong> A single request and a single response (the typical REST pattern).</li>\n<li><strong>Server streaming:</strong> The client sends one request and the server sends back a stream of messages.</li>\n<li><strong>Client streaming:</strong> The client sends a stream of messages and the server responds once.</li>\n<li><strong>Bidirectional streaming:</strong> Both client and server send a stream of messages simultaneously.</li>\n</ul>\n<h3>Streaming as a first-class feature</h3>\n<p>This makes gRPC a strong candidate for real-time applications like chat services, stock tickers, or IoT telemetry feeds.</p>\n<p>In the world of REST, you would typically reach for WebSockets or Server-Sent Events (SSE) to get similar results. These are often treated as \"add-ons\" to the API rather than a core part of the protocol. With gRPC, streaming is built in.</p>\n<h3>A streaming example</h3>\n<p>Imagine a system that processes a large dataset of vector embeddings for an AI application. A client could stream the data to the server, and the server could stream back the results as they are processed.</p>\n<p>This reduces the memory footprint on both ends, because the entire dataset never needs to be loaded into RAM at once.</p>\n<h2>Choosing between REST and gRPC for 2026</h2>\n<p>Base the decision on your \"consumer.\"</p>\n<h3>Humans and browsers: REST</h3>\n<p>If your consumer is a human developer or a web browser, choose REST. The ecosystem of tools, from Postman to Chrome DevTools, is simply too mature to ignore. REST is the language of the public web.</p>\n<h3>Servers and constrained devices: gRPC</h3>\n<p>If your consumer is another server, or a performance-constrained device like a mobile phone or an IoT sensor, gRPC is usually the better choice. The gains in serialization and the reduced network overhead save you money on bandwidth and compute.</p>\n<h3>Both, in one architecture</h3>\n<p>In a modern architecture, it is common to see both living together. A RESTful API gateway handles incoming traffic from the internet, then talks to internal microservices using gRPC.</p>\n<p>If you are building for the web, start with REST. If you are building for the cloud at scale, master gRPC.</p>\n<p>As we move further into <a href=\"https://ansezz.com/blog/ai-vs-traditional-development/\">AI-integrated development</a>, the efficiency of these data pipelines matters even more. Large language models and agentic systems need low-latency access to data, which makes gRPC a natural fit for the \"inner loop\" of AI infrastructure.</p>\n<figure>\n\nDraw your current service map and mark each arrow \"browser or third party\" or \"service to service.\" The first kind stays REST. The busiest arrow of the second kind is your first gRPC candidate.\n\n\n- **Performance:** gRPC is usually faster than REST thanks to binary serialization (Protobuf) and HTTP/2 multiplexing.\n- **Payload size:** Protobuf messages are smaller than JSON because they omit redundant field names and use compact binary encoding, though the gain depends on data shape.\n- **Contract-first:** gRPC requires a strict schema definition, leading to better type safety and less \"API drift\" in large systems.\n- **Interoperability:** REST remains the king of the public web and browser-based clients due to its human-readable JSON format and native browser support.\n- **Streaming:** gRPC supports native bidirectional streaming, making it ideal for real-time data pipelines and IoT.\n- **Hybrid approach:** Many modern architectures use REST for the public-facing edge and gRPC for internal service-to-service communication.\n\n<p>If you're designing services like this for production, <a href=\"https://ansezz.com/services/\">here's how I help teams ship it</a>.</p>\n<p>Are you prepared to handle the added complexity of a contract-first architecture in exchange for smaller payloads and faster serialization?</p>\n</figure></figure>",
      "date_published": "2026-06-14T00:00:00.000Z",
      "date_modified": "2026-06-14T00:00:00.000Z",
      "tags": [
        "architecture",
        "api-design",
        "microservices",
        "devops"
      ],
      "image": "https://ansezz.com/blog/rest-vs-grpc/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/serverless-vs-containers/",
      "url": "https://ansezz.com/blog/serverless-vs-containers/",
      "title": "Serverless vs containers: the 2026 engineering guide",
      "summary": "Compare Serverless vs Containers for performance, cost, and scalability. Learn why hybrid models are winning for Laravel and Shopify applications in 2026.",
      "content_html": "<p>Serverless wins when traffic is spiky and tasks are short. Containers win when traffic is steady and processes run long. Most 2026 stacks use both.</p>\n\n- Serverless (FaaS) hides the whole runtime: it scales per request and to zero, but you pay with cold starts, time limits and a stateless design.\n- Containers package the full runtime: portable, no execution limit and steady latency, at the cost of owning images, scanning and memory.\n- Serverless containers like Google Cloud Run blend the two, so your main app runs as a container while bursty side tasks use functions.\n\n<p>Developers are drowning in YAML files and server patches while the business demands faster shipping. Managing infrastructure often feels like a full-time job that has nothing to do with writing actual code.</p>\n<p>When your application hits a sudden traffic spike during a flash sale or a viral product launch, the \"504 Gateway Timeout\" becomes your worst enemy. If your infrastructure cannot scale in seconds, you lose revenue and customer trust.</p>\n<p>The choice between Serverless vs Containers is a strategic decision as much as a technical one. It sets your operational overhead and your ability to stay lean.</p>\n<p>In 2026, the lines between these technologies have blurred. We see serverless containers and event-driven architectures that mix both worlds. This guide breaks down the engineering substance of Serverless vs Containers to help you decide where to host your next Laravel application or Shopify integration.</p>\n<h2>Understanding the architectural split</h2>\n<p>The debate over Serverless vs Containers is often simplified into \"no servers\" vs \"virtual servers.\" In reality, the difference lies in the abstraction layer.</p>\n<h3>Serverless</h3>\n<p><strong>Serverless</strong> (often called Function-as-a-Service or FaaS) abstracts the entire execution environment. You upload code, and the provider (AWS Lambda, Google Cloud Functions) handles the trigger, the scaling, and the underlying OS.</p>\n<p>It is event-driven by design. Your code stays dormant until an HTTP request, a file upload, or a database change wakes it up.</p>\n<h3>Containers</h3>\n<p><strong>Containers</strong> (Docker, Kubernetes) package the entire runtime environment: the application code, libraries, and system dependencies. You have full control over the operating system version and the configuration.</p>\n<p>Containers run on infrastructure you manage (or semi-manage via Fargate or Cloud Run). They are typically \"always on\" or need specific scaling rules to spin up and down.</p>\n<table>\n<thead>\n<tr>\n<th>Feature</th>\n<th>Serverless (FaaS)</th>\n<th>Containers (Docker/K8s)</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Control</strong></td>\n<td>Minimal; provider-managed</td>\n<td>High; you define the image</td>\n</tr>\n<tr>\n<td><strong>Scaling</strong></td>\n<td>Automatic per request</td>\n<td>Orchestrator-managed</td>\n</tr>\n<tr>\n<td><strong>State</strong></td>\n<td>Strictly stateless</td>\n<td>Can be stateful</td>\n</tr>\n<tr>\n<td><strong>Runtime</strong></td>\n<td>Limited to provider support</td>\n<td>Anything that can be Dockerized</td>\n</tr>\n<tr>\n<td><strong>Timeout</strong></td>\n<td>Usually 15 minutes or less</td>\n<td>No inherent execution limit</td>\n</tr>\n</tbody>\n</table>\n<h2>Serverless: the high-speed execution engine</h2>\n<p>Serverless is the tool for developers who want to ignore infrastructure. The main advantage is the \"scale-to-zero\" model. When no one is using your app, you pay nothing.</p>\n<p>When traffic surges, the provider spins up new instances of your function automatically, within account concurrency limits. AWS Lambda defaults to 1,000 concurrent executions per region (raisable on request) and scales out at roughly 1,000 new environments every 10 seconds.</p>\n<h3>The cold start penalty</h3>\n<p>The catch is the \"cold start.\" When a function hasn't been used recently, the provider must provision a container and boot your runtime.</p>\n<p>Lightweight runtimes like Node.js and Go start fast natively, and provisioned concurrency keeps instances warm. Heavier runtimes feel it more.</p>\n<p>That is why AWS built SnapStart (first for Java, later .NET and Python) to snapshot an initialized runtime and cut cold starts sharply. For frameworks like PHP, cold starts remain a factor that needs careful planning.</p>\n<h3>Perfect for glue code</h3>\n<p>Serverless is perfect for \"glue code.\" If you are building a <a href=\"https://ansezz.com/blog/agentic-commerce-shopify/\">Shopify app architecture</a> that needs to process webhooks or resize images on the fly, serverless functions are very efficient.</p>\n<p>They absorb the bursty, unpredictable nature of webhooks without a dedicated server sitting idle 99% of the time.</p>\n<figure>\n\nOpen your Lambda logs and count the `REPORT` lines that include `Init Duration`. Their share of all invocations is your real cold start rate, and it tells you whether provisioned concurrency or SnapStart is worth paying for.\n\n<h2>Containers: the universal blueprint</h2>\n<p>Containers offer a level of portability that serverless cannot match. A Docker image that runs on your local machine runs the same way on AWS ECS, Google Cloud Run, or a self-hosted <a href=\"https://ansezz.com/blog/coolify-docker-saas-hosting/\">Coolify instance</a>.</p>\n<h3>Long-running work</h3>\n<p>For long-running processes or heavy compute tasks, containers are the standard. If your application needs a persistent WebSocket server, heavy machine learning inference, or long-running cron jobs that exceed 15 minutes, containers are the natural fit.</p>\n<p>(Managed add-ons like API Gateway WebSockets can bridge some of this for functions, but the long-lived process itself belongs in a container.)</p>\n<h3>The operational tax</h3>\n<p>The main drawback is the \"operational tax.\" Even with managed services like AWS Fargate, you are still responsible for your Dockerfiles, image security scanning, and making sure your containers don't run out of memory.</p>\n<p>You are managing the environment, even if you aren't managing the physical hardware.</p>\n<pre><code># A typical Laravel Dockerfile for a containerized approach\nFROM php:8.4-fpm-alpine\n\n# Install system dependencies\nRUN apk add --no-cache libpng-dev libzip-dev zip unzip git\n\n# Install PHP extensions\nRUN docker-php-ext-install pdo_mysql gd zip\n\n# Set working directory\nWORKDIR /var/www\n\n# Copy application code\nCOPY . .\n\n# Install composer dependencies\nRUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer\nRUN composer install --no-dev --optimize-autoloader\n\nCMD [\"php-fpm\"]\n</code></pre>\n<h2>Technical head-to-head: latency and cost</h2>\n<p>When comparing Serverless vs Containers, cost is the most common point of confusion. Serverless looks cheaper because you only pay for what you use. This is true for low-traffic or spiky applications.</p>\n<h3>The cost crossover</h3>\n<p>At a certain level of sustained traffic, containers become more cost-effective. Once your application handles thousands of requests per minute consistently, the per-invocation cost of serverless starts to exceed the monthly cost of a reserved container instance.</p>\n\n  <div>Serverless usually costs less: you pay per request and nothing while idle.</div>\n  <div>Containers usually cost less: a reserved instance beats paying per invocation thousands of times a minute.</div>\n\n<h3>Latency and jitter</h3>\n<p>Latency is the other big trade-off. Containers offer \"warm\" execution. Since the process is already running, the time-to-first-byte (TTFB) is consistent.</p>\n<p>Serverless performance can be \"jittery.\" One request might take 30ms while the next (a cold start) takes several hundred milliseconds.</p>\n<p>For an interactive admin dashboard, this inconsistency can frustrate users. It also ties directly into how you architect <a href=\"https://ansezz.com/blog/stateless-vs-stateful-apps/\">stateless vs stateful apps</a>, since serverless forces a strictly stateless design.</p>\n\nTake last month's request count and average duration, and price them on your provider's function calculator. Then price one always-on container that can handle your peak. The cheaper line tells you which side of the crossover you are on.\n\n<h2>Laravel and Shopify: where to host?</h2>\n<p>For a modern Laravel application, the choice often comes down to <strong>Laravel Vapor</strong> (Serverless) vs <strong>Docker on Cloud Run/Coolify</strong> (Containers).</p>\n<h3>The case for serverless Laravel</h3>\n<p>If you are building a SaaS with unpredictable growth, Laravel Vapor is a strong option. It handles the complexity of deploying a full-stack PHP framework to AWS Lambda.</p>\n<p>It manages your assets on S3 and handles database scaling. It is the \"easy button\" for high-scale Laravel apps that don't want a dedicated DevOps engineer.</p>\n<h3>The case for containerized Laravel</h3>\n<p>If your application is \"always on\" and involves heavy background processing (like syncing thousands of products for a Shopify store), containers are the better fit.</p>\n<p>Docker lets you run your HTTP layer, your queue workers, and your scheduler in one consistent environment. This setup matches your local development environment, which cuts down the \"it works on my machine\" bugs.</p>\n<h2>The 2026 hybrid reality</h2>\n<p>Modern engineering has moved past the binary choice of Serverless vs Containers. We are now in the era of <strong>serverless containers</strong>.</p>\n<p>Services like Google Cloud Run let you package your app as a container and still get the autoscaling and pay-per-use benefits of serverless.</p>\n<h3>A note on AWS</h3>\n<p>AWS App Runner played that role on AWS, but it stopped accepting new customers on April 30, 2026. AWS now points new container apps to ECS Express Mode, which runs on Fargate and does not scale to zero.</p>\n<h3>Mix and match</h3>\n<p>You can deploy your main Laravel API as a container on Cloud Run, which scales to zero when idle. At the same time, you can use pure serverless functions (like AWS Lambda) for specific, isolated tasks like PDF generation or sending transactional emails.</p>\n<p>Your core business logic stays in a predictable container, while your bursty, auxiliary tasks scale independently.</p>\n<p>This hybrid approach puts each workload in the environment that suits it.</p>\n<figure>\n\nList your app's background tasks and mark each one \"short and bursty\" or \"long or steady.\" Move one short, bursty task (PDF generation, image resizing) to a function and leave the rest in your container.\n\n\nChoosing between Serverless vs Containers needs an honest look at your team's skills and your application's traffic patterns.\n<ul>\n<li><strong>Choose Serverless</strong> if your traffic is spiky, you have a small team with no DevOps experience, and your tasks are short-lived (under 15 minutes). It is the fastest path from code to production for event-driven systems.</li>\n<li><strong>Choose Containers</strong> if you have steady, high-volume traffic, require specific OS libraries, or need long-running background processes. It offers the best price-to-performance ratio for \"always-on\" applications.</li>\n<li><strong>Go Hybrid</strong> if you want the best of both worlds. Use a serverless container platform like Cloud Run for your web app and FaaS for your side effects.</li>\n<li><strong>Laravel developers:</strong> Vapor is great for AWS-centric teams, but Dockerized deployments on platforms like Coolify offer more control and lower long-term costs for many regional businesses.</li>\n<li><strong>Shopify developers:</strong> Use serverless functions for webhooks to prevent bottlenecks during peak sales events like Black Friday, but keep your admin UI in a container for consistent speed.\n</li></ul>\n\n<p>If you're weighing serverless against containers, <a href=\"https://ansezz.com/services/\">here's how I help teams make that call</a>.</p>\n<p>If you had to migrate your core API today, would you prioritize the absolute control of a Dockerfile or the operational freedom of an event-driven function?</p>\n</figure></figure>",
      "date_published": "2026-06-14T00:00:00.000Z",
      "date_modified": "2026-06-14T00:00:00.000Z",
      "tags": [
        "architecture",
        "infrastructure",
        "docker",
        "laravel",
        "shopify"
      ],
      "image": "https://ansezz.com/blog/serverless-vs-containers/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/replication-vs-backup-laravel/",
      "url": "https://ansezz.com/blog/replication-vs-backup-laravel/",
      "title": "Replication vs backup: why Laravel needs both",
      "summary": "Replication protects you from hardware failure; backups protect you from your own mistakes. How read/write splitting and point-in-time recovery fit in.",
      "content_html": "<p>Replication copies your data everywhere in milliseconds, including the mistake that just deleted it. Only a backup lets you go back in time.</p>\n\n- Replication is for uptime: replicas take over when hardware fails and can serve reads, but they mirror every bad write instantly.\n- Backups are for recovery: point-in-time snapshots on separate storage are your only way back from bugs, bad migrations and human error.\n- In Laravel, use `read`/`write` connections with `sticky` for replicas, keep real backups beside any Shopify sync, and test your restores.\n\n<p>You deploy a breaking database migration that drops a critical column. Your production database replicates in real time to three nodes across two regions.</p>\n<p>Within milliseconds, that destructive change reaches every one of your \"safety\" copies. You have zero downtime, and that column is gone from every copy.</p>\n<p>This is the moment you realize that replication is not a backup.</p>\n<p>Engineering teams often confuse high availability with data durability. Both matter, but they solve different problems.</p>\n<p>Replication protects you from hardware failure and latency. Backups protect you from logic errors, corruption, and human mistakes.</p>\n<p>If you are building a Laravel application or a high-volume Shopify Plus integration, knowing where one ends and the other begins is the difference between a minor incident and a company-ending event.</p>\n<h2>The fundamental trade-off: speed vs history</h2>\n<p>Replication is about continuity. Its goal is that if your main database server vanishes, a secondary node is ready to take over immediately.</p>\n<p>This is measured in Recovery Time Objective (RTO). In a well-tuned system, failover happens in seconds. You are mirroring every <code>INSERT</code>, <code>UPDATE</code>, and <code>DELETE</code> as they happen.</p>\n<h3>Backups are history</h3>\n<p>Backups are about recovery. Their goal is a safe version of your data from a specific point in history.</p>\n<p>If a bug corrupts your inventory levels at 2:00 PM, a backup from 1:00 PM is your only lifeline. Backups are usually stored as compressed snapshots on detached storage like Amazon S3 or Google Cloud Storage.</p>\n<table>\n<thead>\n<tr>\n<th>Feature</th>\n<th>Replication</th>\n<th>Backup</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Primary Goal</strong></td>\n<td>High Availability (HA)</td>\n<td>Disaster Recovery (DR)</td>\n</tr>\n<tr>\n<td><strong>Data State</strong></td>\n<td>Real-time / Live</td>\n<td>Historical / Snapshot</td>\n</tr>\n<tr>\n<td><strong>Propagation</strong></td>\n<td>Immediate (including errors)</td>\n<td>None (isolated from live changes)</td>\n</tr>\n<tr>\n<td><strong>Storage Cost</strong></td>\n<td>High (hot, identical hardware)</td>\n<td>Low (cold, compressed snapshots)</td>\n</tr>\n<tr>\n<td><strong>Recovery Speed</strong></td>\n<td>Seconds to Minutes</td>\n<td>Minutes to Hours</td>\n</tr>\n</tbody>\n</table>\n\nWrite down two numbers for your main database: how long you can be down (RTO) and how much data you can lose to a bad write (RPO: how old your newest restorable backup can be). Replication covers the first. Only backups cover the second.\n\n<h2>Why replication faithfully mirrors your mistakes</h2>\n<p>The greatest strength of replication is also its greatest weakness. It is designed to be a mirror.</p>\n<p>If you run a <code>truncate</code> command on your primary database, the replication protocol assumes it was intentional. It will purge that data from your replicas before you can even hit \"Ctrl+C\" in your terminal.</p>\n<h3>A real failure</h3>\n<p>I have seen developers rely solely on RDS Read Replicas as their \"safety net.\" When a rogue queue job began overwriting customer email addresses with null values, the replicas updated instantly.</p>\n<p>Without a point-in-time backup, those original emails were gone forever. Replication provides infrastructure resilience, not data integrity.</p>\n<h3>Separate the two jobs</h3>\n<p>For a production-grade <a href=\"https://ansezz.com/blog/coolify-self-hosted-saas/\">Coolify self-hosted SaaS</a>, you must separate your concerns.</p>\n<p>Use replication to scale your reads and handle node failures. Use automated, encrypted backups so you can roll back to a known-good state.</p>\n<figure>\n<h2>Implementing replication in Laravel</h2>\n<p>Laravel handles read/write splitting through native configuration. By defining <code>read</code> and <code>write</code> connections in your <code>config/database.php</code>, the framework automatically routes <code>SELECT</code> statements to your replicas while sending <code>INSERT</code> and <code>UPDATE</code> statements to the primary.</p>\n<p>This is the same connection layer you tune when designing a <a href=\"https://ansezz.com/blog/laravel-multi-tenancy/\">multi-tenant Laravel architecture</a>.</p>\n<pre><code>'mysql' =&gt; [\n    'read' =&gt; [\n        'host' =&gt; [\n            '192.168.1.10', // Replica 1\n            '192.168.1.11', // Replica 2\n        ],\n    ],\n    'write' =&gt; [\n        'host' =&gt; [\n            '192.168.1.1', // Primary\n        ],\n    ],\n    'sticky' =&gt; true,\n    'driver' =&gt; 'mysql',\n    // ... other settings\n],\n</code></pre>\n<h3>Why <code>sticky</code> matters</h3>\n<p>The <code>sticky</code> option is important here. If you write a record during a request, any later reads during that same request come from the primary.</p>\n<p>This avoids \"read-after-write\" lag, where a replica is a few milliseconds behind the primary and your application looks as if the data vanished.</p>\n\nCheck your `config/database.php`. If you have `read` hosts but no `'sticky' =&gt; true`, add it, then test a \"create and redirect to the new record\" flow under load.\n\n<h2>Shopify Plus and the sync fallacy</h2>\n<p>In the world of Shopify Plus, many developers build \"replication-style\" sync engines. They listen for <code>orders/create</code> or <code>products/update</code> webhooks and mirror that data into a local Laravel database.</p>\n<p>This is a useful pattern for custom reporting or complex <a href=\"https://ansezz.com/blog/agentic-commerce-shopify/\">agentic commerce workflows</a>.</p>\n<h3>The sync deletes too</h3>\n<p>However, this local database is often mistaken for a backup. If an admin deletes a collection of products in the Shopify admin panel, Shopify fires a webhook.</p>\n<p>Your Laravel app receives that webhook and promptly deletes those products from your local DB to stay \"in sync.\"</p>\n\n  <div>A delete in Shopify becomes a delete in your local DB. Your copy is as short-lived as the live data.</div>\n  <div>Daily snapshots of the local DB, plus versioned JSON payloads in S3, let you rebuild state even after the sync deletes everything.</div>\n\n<h3>Build real history</h3>\n<p>If you do not have a separate backup strategy that creates daily snapshots of your local DB, your local data is just as ephemeral as the live Shopify data.</p>\n<p>To build true resilience, store historical JSON payloads of those Shopify resources in a versioned storage system like S3. This lets you reconstruct your state even if the live sync deletes everything.</p>\n<figure>\n<h2>Cloud infrastructure and the cost of availability</h2>\n<p>Managing replication by hand is a DevOps nightmare. For most Laravel applications, managed services like AWS RDS or Google Cloud SQL are the correct move. They handle the binary log replication, failover orchestration, and monitoring out of the box.</p>\n<h3>Multi-AZ is not a read replica</h3>\n<p>When configuring your cloud DB, you will see \"Multi-AZ\" (AWS) or \"High Availability\" (GCP) settings.</p>\n<p>A single-standby Multi-AZ deployment uses synchronous replication. It roughly doubles your database cost, because you run a standby instance in a second Availability Zone that does nothing but wait for the primary to die.</p>\n<p>This standby cannot serve read traffic. To scale reads you need separate read replicas (or RDS Multi-AZ DB clusters), not the HA standby.</p>\n<h3>Backups are cheap, testing is not</h3>\n<p>Backups, by contrast, are very cheap. Storing 500GB of compressed dumps in S3 Standard costs roughly $11 to $12 a month. Archival tiers like Glacier Deep Archive drop that to around 50 cents, at the cost of multi-hour retrieval.</p>\n<p>The cost of a backup is not in the storage; it is in the testing. A backup that has never been restored is just a collection of random bits. You must automate your restore tests.</p>\n<p>Time the download too: my <a href=\"https://ansezz.com/tools/transfer-time-calculator/\">transfer time calculator</a> shows how long 500GB takes on your link.</p>\n<p>I recommend using <a href=\"https://ansezz.com/blog/coolify-docker-saas-hosting/\">Coolify and Docker</a> to spin up ephemeral environments where you can test your backup restoration process regularly without touching production.</p>\n\nSchedule a monthly job that restores last night's backup into a throwaway container, runs `php artisan migrate:status` and a row count on your key tables, then deletes the container. Alert if any step fails.\n\n\n- **Replication is for uptime.** It allows your app to stay online during hardware failures or network partitions.\n- **Backups are for survival.** They are your only defense against data corruption, malicious attacks, and developer errors.\n- **Replication propagates bugs.** If your code breaks the data, the replica will break just as fast.\n- **Laravel supports read/write splitting.** Use the `sticky` configuration to prevent consistency issues during web requests.\n- **Shopify webhooks are not backups.** A synchronized local database is a replica, not a historical record.\n- **Test your restores.** Automate a process to verify that your backup snapshots actually work.\n\n<p>If you're architecting that resilience layer for production, <a href=\"https://ansezz.com/services/\">here's how I help teams ship it</a>.</p>\n<p>If you had to choose between a system with 99.99% uptime but no backups, and a system with 95% uptime but hourly backups, which one would keep you from losing your business?</p>\n</figure></figure>",
      "date_published": "2026-06-13T00:00:00.000Z",
      "date_modified": "2026-06-13T00:00:00.000Z",
      "tags": [
        "devops",
        "databases",
        "laravel",
        "shopify",
        "infrastructure"
      ],
      "image": "https://ansezz.com/blog/replication-vs-backup-laravel/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/rag-vs-fine-tuning/",
      "url": "https://ansezz.com/blog/rag-vs-fine-tuning/",
      "title": "RAG vs fine-tuning: choosing your AI architecture",
      "summary": "RAG vs fine-tuning for production LLMs: knowledge vs behavior, latency, cost, privacy, and the hybrid approach. How to pick the right AI architecture.",
      "content_html": "<p>RAG gives your model an open book at runtime. Fine-tuning changes how the model behaves. Use RAG for facts and fine-tuning for style.</p>\n\n- RAG retrieves relevant chunks from your own data at request time, so answers stay fresh and access control stays in your database.\n- Fine-tuning retrains the model's weights on a focused dataset to lock in tone, terminology and response patterns, with lower latency per request.\n- Many production systems combine them: a fine-tuned model for behavior, RAG for live facts.\n\n<p>Large language models are only as good as the information they can access. You build a custom application and realize the base model knows nothing about your internal documentation or yesterday's product launch. It starts making things up.</p>\n<p>This hallucination problem is the biggest hurdle for developers moving from a prototype to production, and it is exactly where the RAG vs fine-tuning decision gets made.</p>\n<p>You can either give the model a search engine to find the right information at runtime, or retrain the model to bake that information into its weights. These two methods are Retrieval-Augmented Generation (RAG) and fine-tuning.</p>\n<p>Choosing the wrong one wastes engineering hours and runs up infrastructure bills. A RAG system might be too slow for a high-traffic API. A fine-tuned model might become obsolete the moment you update your pricing page.</p>\n<h2>Understanding RAG as the open-book exam</h2>\n<p>Retrieval-Augmented Generation is like giving an LLM an open-book exam. Instead of relying on the model to remember everything from its original training, you provide relevant documents along with the user prompt.</p>\n<p>The model then uses this context to generate an accurate answer.</p>\n<h3>How the pipeline works</h3>\n<p>The workflow has a few moving parts. First, you convert your documents into numerical representations called embeddings and store them in a vector database like pgvector or Pinecone.</p>\n<p>When a user asks a question, your system runs a semantic search to find the most relevant \"chunks\" of text. These chunks are then injected into the prompt window of the LLM.</p>\n<h3>Built for changing data</h3>\n<p>RAG is the standard choice for applications where data changes frequently. If you are building a support bot for a Shopify store, it needs to know about current inventory levels.</p>\n<p>Retraining a model every time a product sells out is not practical. With RAG, you just update the vector index. You can read more about avoiding common pitfalls in our guide on <a href=\"https://ansezz.com/blog/7-rag-mistakes-production/\">7 RAG mistakes in production</a>.</p>\n<figure>\n<h2>Fine-tuning as internalized knowledge</h2>\n<p>Fine-tuning takes a pre-trained model and continues its training on a smaller, specialized dataset. You are changing the \"brain\" of the model by adjusting its internal weights.</p>\n<p>The model then picks up the patterns, tone, and terminology of your domain without needing external documents for every query.</p>\n<h3>Behavior, not facts</h3>\n<p>This approach is less about adding new facts and more about teaching the model how to act. It shines when you need a consistent style, tone, or response pattern that prompting alone cannot pin down reliably.</p>\n<p>(For strict JSON shape, reach for structured outputs or constrained decoding first. Every major provider now guarantees schema-valid output at inference time, no training run required.)</p>\n<h3>Specialized fields, frozen facts</h3>\n<p>Fine-tuning is also common in specialized industries like legal or medical tech, where the model learns the specific \"language\" of the field.</p>\n<p>The major drawback is that it creates a static snapshot of knowledge. If the underlying facts change, the model stays stuck with its training data until you run another expensive training cycle.</p>\n\n  <div>Changes what the model can see. Update the index and the next answer uses the new facts.</div>\n  <div>Changes how the model behaves. New facts need a new training run.</div>\n\n\nSort your last 20 bad model answers into two piles: \"wrong or missing facts\" and \"right facts, wrong tone or format.\" The first pile is a RAG problem. The second is a prompting or fine-tuning problem.\n\n<h2>The latency and throughput battle</h2>\n<p>Performance is where these two architectures split sharply. RAG adds several steps to every single request.</p>\n<p>Your application must embed the query, search the vector database, and then send a much larger prompt (with the retrieved text) to the LLM. Each step adds milliseconds or even seconds to the total response time.</p>\n<h3>When latency is a deal-breaker</h3>\n<p>For high-traffic applications, this latency can be a deal-breaker. The retrieval step also means standing up and querying a vector store alongside your app. See <a href=\"https://ansezz.com/blog/picking-the-right-rag-stack/\">picking the right RAG stack</a> for how that choice shapes both speed and cost.</p>\n<h3>Fine-tuning is lean at runtime</h3>\n<p>Fine-tuning offers much lower latency at runtime. There is no external retrieval step, and the prompt stays short because the model already \"knows\" the context.</p>\n<p>This makes fine-tuned models a good fit for real-time applications or high-volume background tasks like sentiment analysis or lead scoring.</p>\n<figure>\n<h2>Infrastructure and cost considerations</h2>\n<p>The cost of RAG is mostly operational. You pay for storing and querying the vector database.</p>\n<p>You also pay more for LLM tokens because your prompts are much longer. Over millions of requests, these \"context tokens\" add up quickly. RAG is cheaper to set up but can become more expensive at extreme scale.</p>\n<h3>Fine-tuning pays up front</h3>\n<p>Fine-tuning has a high upfront cost. You need to curate a high-quality dataset, which often requires human labeling, and significant GPU power to run the training.</p>\n<p>Once the model is trained, your per-query costs are lower. The prompts are shorter and you do not need to maintain a complex retrieval pipeline.</p>\n<h3>Which world can your team run?</h3>\n<p>The deeper split is operational. Fine-tuning lives in the <a href=\"https://ansezz.com/blog/training-vs-inference/\">training</a> world (GPUs, datasets, MLOps), while RAG lives in the inference-time data-pipeline world.</p>\n<p>Decide which world your team is actually equipped to run.</p>\n\nMeasure the average prompt size of your RAG calls in tokens, multiply by your monthly request count and your model's input price. That is your monthly \"context tax,\" the number to compare against a fine-tuning run.\n\n<h2>Data privacy and governance</h2>\n<p>For enterprise clients, data privacy is often the deciding factor. RAG offers better data governance.</p>\n<p>Since the information is retrieved from your own database at runtime, you can apply standard access controls and make sure a user only retrieves documents they are authorized to see. The LLM never \"learns\" the data permanently.</p>\n<h3>The memorization risk</h3>\n<p>Fine-tuning is riskier here. If you train a model on sensitive customer data, that information is baked into the model weights.</p>\n<p>Membership inference and training-data extraction attacks are well-documented ways to pull memorized data back out.</p>\n<p>This makes it hard to \"delete\" a user's data from a fine-tuned model short of retraining, which can lead to compliance issues under regulations like GDPR.</p>\n<h2>The hybrid case: RAG and fine-tuning together</h2>\n<p>In 2026, many advanced systems do not choose one over the other. They combine both.</p>\n<p>You might fine-tune a smaller, cheaper model (like a Llama variant) to learn your company's specific API schemas and brand voice. Then you use RAG to give that model access to live data.</p>\n<h3>The \"how\" and the \"what\"</h3>\n<p>This combination lets the model be both fast and accurate. Fine-tuning handles the \"how\" (formatting and style) while RAG handles the \"what\" (current facts and data).</p>\n<p>This works especially well with <a href=\"https://ansezz.com/blog/claude-mcp-dev-tools/\">Claude and the Model Context Protocol (MCP)</a>, where the model can use specific tools to fetch data while keeping a trained understanding of the environment.</p>\n\nBefore you fine-tune anything, write a strong system prompt plus five example answers in your target style and run it through your RAG pipeline. If that closes the gap, you may not need a training run at all.\n\n\nChoosing between RAG and fine-tuning depends on your goals for data freshness, latency, and budget.\n<ul>\n<li><strong>Use RAG for knowledge:</strong> If your data changes frequently or you need to cite sources, RAG is the standard choice. It is easier to update and more transparent.</li>\n<li><strong>Use fine-tuning for behavior:</strong> If you need a consistent style, a unique brand voice, or low-latency responses, fine-tuning is the way to go. For strict output formats, try structured outputs first.</li>\n<li><strong>Evaluate your infrastructure:</strong> RAG requires a reliable data pipeline and vector database. Fine-tuning requires specialized training data and GPU resources.</li>\n<li><strong>Consider privacy early:</strong> RAG is generally safer for handling sensitive or permission-based data because the model does not memorize the information.</li>\n<li><strong>Think hybrid for scale:</strong> Combining a fine-tuned model for behavior with a RAG pipeline for facts gives complex applications the strongest results.</li>\n<li><strong>Monitor costs:</strong> Watch your token usage in RAG systems as context windows grow. Conversely, budget for periodic retraining if you choose the fine-tuning path.\n</li></ul>\n\n<p>If you're deciding between these paths for a real product, <a href=\"https://ansezz.com/services/\">here's how I help teams ship it</a>.</p>\n<p>Is your current AI architecture bottlenecked by the speed of retrieval or the accuracy of the model's internal knowledge?</p>\n</figure></figure>",
      "date_published": "2026-06-12T00:00:00.000Z",
      "date_modified": "2026-06-12T00:00:00.000Z",
      "tags": [
        "ai",
        "rag",
        "machine-learning",
        "llm",
        "architecture",
        "laravel"
      ],
      "image": "https://ansezz.com/blog/rag-vs-fine-tuning/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/rate-limiting-vs-throttling/",
      "url": "https://ansezz.com/blog/rate-limiting-vs-throttling/",
      "title": "Rate limiting vs throttling",
      "summary": "The engineering difference between rate limiting and throttling, and how each secures your APIs and keeps infrastructure stable under heavy load.",
      "content_html": "<p>Rate limiting says \"no\" once you go over the line. Throttling says \"slow down.\" Pick the wrong one and your users get errors they didn't need, or your database melts anyway.</p>\n\n- Rate limiting is a policy: X requests per Y time, then an instant HTTP 429. Use it for fairness, quotas and blocking abuse.\n- Throttling shapes traffic: it delays or queues requests so a spike drains at a pace your backend can handle.\n- Use both: rate limit at the edge, throttle inside your app around databases and third-party APIs, and track the counters in Redis.\n\n<p>Your server is screaming, not because of a bug, but because of a success. A viral tweet or a massive batch of automated requests has flooded your API.</p>\n<p>Without rate limiting or throttling in place, your database locks up, your CPU pegs at 100%, and the site goes down for everyone.</p>\n<p>This is the nightmare scenario for any engineering team. To prevent it, you need to control the flow of traffic.</p>\n<p>Engineers often use \"rate limiting\" and \"throttling\" interchangeably. Both deal with traffic management, but they solve the problem with different philosophies.</p>\n<p>Choosing the wrong one can lead to a degraded user experience or, worse, a complete system failure under pressure. This guide breaks down both strategies and how to implement them in stacks like Laravel and Shopify.</p>\n<h2>The traffic surge crisis</h2>\n<p>The problem starts with resources. Every server has a finite amount of memory, compute, and bandwidth.</p>\n<p>When requests come in faster than your system can process them, a queue builds up. Once that queue exceeds the system's capacity, the service fails.</p>\n<h3>Not only attackers</h3>\n<p>This is not only about malicious actors or DDoS attacks. It is often about \"noisy neighbors\" in a multi-tenant environment, or a poorly optimized loop in a client-side application.</p>\n<p>I have seen production environments crash simply because a mobile app's \"retry\" logic was too aggressive during a minor network hiccup.</p>\n<p>Rate limiting and throttling are your tools to enforce discipline. They make sure no single user or service can monopolize your infrastructure. They are the gatekeepers of your <a href=\"https://ansezz.com/blog/coolify-docker-saas-hosting/\">cloud infrastructure</a>.</p>\n<h2>Defining rate limiting: the hard stop</h2>\n<p>Rate limiting is a policy-based approach. It defines a strict contract: \"You are allowed exactly X requests per Y amount of time.\" Once a user reaches that limit, the gate shuts.</p>\n<p>When a request exceeds the limit, the server rejects it immediately, typically with an HTTP <a href=\"https://ansezz.com/tools/http-status-codes/\">429 \"Too Many Requests\" status code</a>.</p>\n<p>The server spends no more resources on that request. It does not look up data in the database. It just says \"no.\"</p>\n<h3>Why use rate limiting?</h3>\n<p>Rate limiting is mainly about fairness and security. It protects your <a href=\"https://ansezz.com/blog/api-gateway-ai-stack/\">API gateway</a> from abuse.</p>\n<p>If you run a SaaS, you might have different tiers. A free user gets 60 requests per minute, while a premium user gets 1,000.</p>\n<h3>Easy to explain</h3>\n<p>Rate limiting is easy to communicate to users. They can check their response headers (like <code>X-RateLimit-Limit</code> and <code>X-RateLimit-Remaining</code>) to see exactly where they stand.</p>\n<p>It is a binary state: you are either within your limit or you are blocked.</p>\n<h2>Defining throttling: the gentle brake</h2>\n<p>Throttling is a runtime behavior designed to \"shape\" traffic. Instead of a hard rejection, throttling slows down the processing of requests.</p>\n<p>Think of it like a funnel. You can pour a bucket of water into it all at once, but it only drips out at a controlled, steady speed.</p>\n<h3>Delay and queue</h3>\n<p>In a throttled system, if you send too many requests, the server might add a delay to each response. It might queue the requests and process them as capacity becomes available.</p>\n<p>The goal is to smooth out spikes and avoid a \"jagged\" traffic pattern.</p>\n<h3>Why use throttling?</h3>\n<p>Throttling is excellent for protecting backend resources like databases or third-party APIs. If you know your database can only handle 500 writes per second, you might throttle incoming requests to stay just under that limit.</p>\n<p>It gives a better experience during occasional spikes. Instead of a 429 error, the user might just see a slightly longer loading time.</p>\n<p>If the traffic stays high for too long, though, the queue eventually fills up, and the system has to start rejecting requests anyway.</p>\n\n  <div>Over the limit? Instant 429. The request costs you almost nothing and the client knows exactly why.</div>\n  <div>Over the pace? The request waits in line. The user sees a slower response instead of an error, until the queue fills.</div>\n\n<figure>\n<h2>Key algorithms: token vs leaky bucket</h2>\n<p>To implement these strategies, engineers rely on specific models. Knowing them helps you tune your system.</p>\n<h3>1. Token bucket (common for rate limiting)</h3>\n<p>Imagine a bucket that holds \"tokens.\" Every time a request comes in, a token is removed. If the bucket is empty, the request is rejected. Tokens are added back to the bucket at a constant rate.</p>\n<ul>\n<li><strong>Pros:</strong> It allows for \"burstiness.\" If the bucket is full, a user can send a quick burst of requests until the tokens run out.</li>\n<li><strong>Cons:</strong> Hard to manage if the burst is too large for your downstream services.</li>\n</ul>\n<h3>2. Leaky bucket (common for throttling)</h3>\n<p>Imagine a bucket with a small hole at the bottom. Requests are poured into the bucket. They \"leak\" out of the hole at a constant rate to be processed. If the bucket overflows, new requests are dropped.</p>\n<ul>\n<li><strong>Pros:</strong> It ensures a stable, predictable flow of traffic to your backend.</li>\n<li><strong>Cons:</strong> It is very strict. It does not let output burst even if the system has idle capacity.</li>\n</ul>\n\nLook up the real limit of your most fragile dependency (database writes per second, a partner API's quota). Set your throttle a little under it, and set your public rate limit so that even every user at their max cannot exceed it.\n\n<h2>Practical implementation: Laravel and Shopify</h2>\n<p>How does this look in the real world? Here are two ecosystems where these patterns matter.</p>\n<h3>Rate limiting in Laravel</h3>\n<p>Laravel makes rate limiting simple through its <code>RateLimiter</code> facade and the <code>throttle</code> middleware. The built-in limiter uses a fixed-window counter backed by your cache store.</p>\n<p>In Laravel 11 and 12, you define named limiters in the <code>boot</code> method of <code>AppServiceProvider</code> (the old <code>RouteServiceProvider</code> was dropped from the default skeleton):</p>\n<pre><code>use Illuminate\\Cache\\RateLimiting\\Limit;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Facades\\RateLimiter;\n\nRateLimiter::for('api', function (Request $request) {\n    return Limit::perMinute(60)-&gt;by($request-&gt;user()?-&gt;id ?: $request-&gt;ip());\n});\n</code></pre>\n<p>This tells Laravel to allow 60 requests per minute per user ID or IP address. If the limit is hit, Laravel throws a <code>ThrottleRequestsException</code>, which results in a 429 response.</p>\n<p>Despite the middleware's name, this is a classic \"hard stop\" rate limit.</p>\n<h3>The Shopify API's leaky bucket</h3>\n<p>Shopify uses a leaky bucket algorithm for its GraphQL Admin API on every plan, not just Plus.</p>\n<p>When you make a GraphQL call, the response carries cost details under <code>extensions.cost</code>, including the <code>requestedQueryCost</code> and a <code>throttleStatus</code>.</p>\n<ul>\n<li><strong>GraphQL cost:</strong> Each query is assigned a calculated cost based on the fields and connection sizes you request.</li>\n<li><strong>The bucket:</strong> Your app has a bucket of \"points\" that restores at 100 points per second on Standard, 200 on Advanced, and 1,000 on Plus. Points are spent on each query and restore over time.</li>\n<li><strong>The wall:</strong> If you spend points faster than they restore, Shopify rejects the query with a <code>THROTTLED</code> error.</li>\n</ul>\n<h3>The throttling is your job</h3>\n<p>Strictly speaking, Shopify itself rejects an over-budget query. The leaky bucket sets the pace, and the throttling (slowing down) is what your client has to do to match it.</p>\n<p>For high-volume commerce, that means building back-off logic into your application. A well-behaved client reads <code>throttleStatus.currentlyAvailable</code> and slows down before it hits the wall, rather than firing blindly and retrying on every <code>THROTTLED</code> error.</p>\n<p>When you do get throttled, wait long enough for <code>restoreRate</code> to refill the points your next query needs.</p>\n<table>\n<thead>\n<tr>\n<th>Feature</th>\n<th>Rate Limiting (Laravel Default)</th>\n<th>Throttling (Shopify-style leaky bucket)</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Primary Action</strong></td>\n<td>Block (HTTP 429)</td>\n<td>Client delays or queues to match the leak rate</td>\n</tr>\n<tr>\n<td><strong>Best For</strong></td>\n<td>Security &amp; Quotas</td>\n<td>Resource Stability</td>\n</tr>\n<tr>\n<td><strong>Logic</strong></td>\n<td>Fixed Window</td>\n<td>Leaky Bucket</td>\n</tr>\n<tr>\n<td><strong>User Experience</strong></td>\n<td>Instant Error</td>\n<td>Latency Spike</td>\n</tr>\n</tbody>\n</table>\n\nLog `extensions.cost.throttleStatus.currentlyAvailable` on every Shopify GraphQL call for a day. If it ever drops near zero, add a pause before the next call when the available points are below that query's cost.\n\n<h2>Architectural impact: security vs experience</h2>\n<p>When designing your system, you must decide where to place these controls.</p>\n<h3>At the edge</h3>\n<p>Implement rate limiting at the WAF or API gateway level. This stops malicious traffic before it even touches your application code, which saves money on compute.</p>\n<h3>Inside the application</h3>\n<p>Implement throttling within your service logic. Use it when calling external APIs or writing to a shared database. Use a queue system like Redis or Amazon SQS to hold requests that exceed your immediate capacity.</p>\n<p>Rate limiting at the perimeter blocks the \"bad actors,\" while internal throttling keeps your services from melting down during a legitimate surge.</p>\n<p>I have found that the most resilient systems use both. This is especially important when building <a href=\"https://ansezz.com/blog/agentic-commerce-shopify/\">agentic commerce systems</a> that may generate many automated API calls in a short period.</p>\n<p>If those calls hit a paid LLM, the stakes shift from CPU to your bill. See <a href=\"https://ansezz.com/blog/rate-limiting-ai-wallet/\">rate limiting and the denial-of-wallet problem</a> for the token-cost angle.</p>\n<figure>\n\nCheck what your 429 responses send back. If there is no `Retry-After` header, add one, so well-behaved clients wait the right amount instead of retrying in a tight loop.\n\n\nManaging traffic is about balance. You want a fast experience for users while keeping your infrastructure healthy.\n<ol>\n<li><strong>Rate limiting is for policy.</strong> Use it to enforce subscription tiers and block brute-force attacks.</li>\n<li><strong>Throttling is for stability.</strong> Use it to smooth out traffic spikes and protect your database or external API dependencies.</li>\n<li><strong>Return 429 codes.</strong> Always let the client know they are being limited. Include a <code>Retry-After</code> header so they know when they can try again. My <a href=\"https://ansezz.com/tools/http-headers/\">HTTP headers cheat sheet</a> shows example values.</li>\n<li><strong>Monitor your limits.</strong> Use dashboards to track how often users hit their limits. If 20% of your users are being limited, your limits might be too low, or your app might be inefficient.</li>\n<li><strong>Use Redis for state.</strong> Both rate limiting and throttling require a fast, central store to track request counts. Redis is the industry standard for this.\n</li></ol>\n\n<p>If you're hardening an API for production, <a href=\"https://ansezz.com/services/\">here's how I help teams ship it</a>.</p>\n<p>How do you decide between rejecting a request immediately or making the user wait 500ms longer to maintain system stability?</p>\n</figure></figure>",
      "date_published": "2026-06-12T00:00:00.000Z",
      "date_modified": "2026-06-12T00:00:00.000Z",
      "tags": [
        "devops",
        "api-design",
        "laravel",
        "shopify",
        "infrastructure",
        "devops"
      ],
      "image": "https://ansezz.com/blog/rate-limiting-vs-throttling/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/rag-architectures-traditional-agentic-corrective/",
      "url": "https://ansezz.com/blog/rag-architectures-traditional-agentic-corrective/",
      "title": "RAG architectures: traditional, agentic, corrective",
      "summary": "Compare traditional, agentic, and corrective RAG architectures, with the latency, cost, and accuracy trade-offs that decide which fits your AI app.",
      "content_html": "<p>Traditional RAG retrieves once and hopes. Agentic RAG plans and loops. Corrective RAG checks the context before it answers. Each step up costs more latency and tokens.</p>\n\n- Traditional RAG is one search and one LLM call: fast and cheap, but it has no way to notice when the retrieved context is wrong.\n- Agentic RAG puts an LLM agent in a plan-act-observe loop that can split questions, call tools and search again, at higher cost and latency.\n- Corrective RAG (CRAG) adds a lightweight evaluator that grades retrieval and falls back to refinement or web search before generating.\n\n<p>Most developers build a basic RAG system only to find it hallucinating or failing on complex questions within a week of deployment.</p>\n<p>If your vector search returns the wrong context, your LLM will confidently lie to your face. This gap between basic search and reliable answers is why RAG architectures have grown from simple pipelines into self-correcting, agentic systems.</p>\n<p>The problem with many first AI implementations is their static nature. You feed a query into a vector database, pull some chunks, and hope the LLM makes sense of them.</p>\n<p>This is the traditional RAG model. It works for simple FAQs, but it breaks down when a query needs multi-step reasoning or when the retrieved data is irrelevant. The result is \"garbage in, garbage out\": the model burns tokens trying to answer from junk context.</p>\n<p>This guide compares three RAG architectures so you can pick the right one for your workload: the speed of a traditional pipeline, the reasoning of agentic RAG, or the verification of corrective RAG (CRAG).</p>\n<h2>Traditional RAG: the linear standard</h2>\n<p>Traditional RAG is the foundation of most AI-driven applications. It follows a strictly linear, one-shot path: retrieve, then generate.</p>\n<p>You start by converting your documents into vector embeddings and storing them in a database like pgvector or Pinecone. When a user asks a question, the system embeds the query, runs a similarity search, and injects the top results into the prompt.</p>\n<h3>Fast and cheap</h3>\n<p>This architecture is prized for its low latency and simplicity. If you are building a simple internal search tool for a Shopify store or a basic documentation bot, Traditional RAG is often enough.</p>\n<p>It is cost-effective because it typically involves only one LLM call and one vector search.</p>\n<h3>No way to notice bad context</h3>\n<p>Its simplicity is also its biggest weakness. It assumes the first retrieval always succeeds.</p>\n<p>If the vector search returns noise, the LLM has no mechanism to tell that the context is wrong. It will try to answer regardless. This often leads to the <a href=\"https://ansezz.com/blog/7-rag-mistakes-production/\">7 RAG mistakes in production</a> that plague early-stage AI projects.</p>\n<h3>Key components of traditional RAG</h3>\n<ul>\n<li><strong>Vector store:</strong> holds document chunks as embeddings.</li>\n<li><strong>Retriever:</strong> a similarity search function that pulls the top-k relevant chunks.</li>\n<li><strong>Generator:</strong> the LLM that synthesizes an answer from the retrieved chunks.</li>\n</ul>\n\nLog the top-k chunks next to every answer for a day. Read 20 of the bad answers. If the right chunk was never retrieved, your problem is retrieval, and a better prompt will not fix it.\n\n<h2>Agentic RAG: the strategic pivot</h2>\n<p>Agentic RAG turns retrieval from a passive pipeline into an active control loop. Instead of a fixed sequence, an LLM agent acts as a \"brain\" that manages the whole workflow.</p>\n<p>The agent can plan its approach, decide which tools to use, and iterate until it finds a satisfactory answer.</p>\n<figure>\n<h3>Splitting the question</h3>\n<p>In an Agentic RAG system, the agent might decide that a single search is not enough. It might break a complex request into three sub-queries, search different data sources for each, and then combine the results into one answer.</p>\n<p>This is particularly useful for <a href=\"https://ansezz.com/blog/agentic-commerce-shopify/\">agentic commerce on Shopify</a>, where a user might ask for a comparison between several products across different categories.</p>\n<h3>The plan-act-observe cycle</h3>\n<p>The core of this architecture is the plan-act-observe cycle (the same loop behind ReAct-style agents). The agent plans a step, performs an action (like calling a search tool), observes the result, and decides whether it needs more information.</p>\n<p>This lets it solve multi-hop problems, where the answer to the first part of a question provides the search terms for the second.</p>\n<h3>The price of loops</h3>\n<p>Agentic RAG is more capable, but it is also more expensive and slower than traditional methods. Each iteration requires another LLM call, which increases both the token cost and the time the user waits.</p>\n<p>Managing these loops takes solid infrastructure, often including an <a href=\"https://ansezz.com/blog/api-gateway-ai-stack/\">API gateway in the AI stack</a> to handle the extra traffic and orchestration complexity.</p>\n<h2>Corrective RAG (CRAG): the self-healing layer</h2>\n<p>Corrective RAG, or CRAG, adds a self-correction step to retrieval. Its goal is to cut hallucinations by putting a lightweight retrieval evaluator between the retrieval and generation steps to judge how relevant the retrieved documents are.</p>\n<p>The original <a href=\"https://arxiv.org/abs/2401.15884\">CRAG paper</a> uses a fine-tuned T5-large model as that evaluator.</p>\n<figure>\n<h3>Three confidence levels</h3>\n<p>The evaluator sorts each retrieval into one of three confidence levels: correct, incorrect, or ambiguous.</p>\n<ul>\n<li><strong>Correct:</strong> CRAG keeps the retrieved context but refines it with a decompose-then-recompose step that strips out irrelevant text before generation.</li>\n<li><strong>Incorrect:</strong> CRAG discards the retrieval and triggers a large-scale web search to pull in more reliable knowledge.</li>\n<li><strong>Ambiguous:</strong> CRAG combines both, using refined retrieval plus web search results.</li>\n</ul>\n<h3>Grounded verification</h3>\n<p>CRAG suits high-stakes environments where accuracy is non-negotiable. It adds a layer of grounded verification to the otherwise probabilistic behavior of LLMs, deciding whether to trust, refine, or replace retrieved context.</p>\n<p>By checking whether the ground truth is actually present in the retrieved data, CRAG keeps the model from inventing facts when the database comes up empty.</p>\n<h3>How the CRAG loop works</h3>\n<ol>\n<li><strong>Retrieve:</strong> fetch initial context chunks.</li>\n<li><strong>Evaluate:</strong> a lightweight evaluator scores each chunk's relevance into correct, incorrect, or ambiguous.</li>\n<li><strong>Correct:</strong> if confidence is low, trigger a secondary retrieval (commonly a web search) and refine the context.</li>\n<li><strong>Generate:</strong> synthesize the final answer only from verified or corrected context.</li>\n</ol>\n<h2>Comparison: speed vs accuracy vs reasoning</h2>\n<p>Choosing between these RAG architectures is a trade-off between performance, cost, and complexity. Use the table below to guide your decision.</p>\n<p>If you are still settling on the storage layer underneath, <a href=\"https://ansezz.com/blog/picking-the-right-rag-stack/\">picking the right RAG stack</a> covers the vector database choices in depth.</p>\n<table>\n<thead>\n<tr>\n<th>Feature</th>\n<th>Traditional RAG</th>\n<th>Agentic RAG</th>\n<th>Corrective RAG</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Flow Type</strong></td>\n<td>Linear</td>\n<td>Cyclic / Iterative</td>\n<td>Evaluative Loop</td>\n</tr>\n<tr>\n<td><strong>Latency</strong></td>\n<td>Low</td>\n<td>High</td>\n<td>Medium</td>\n</tr>\n<tr>\n<td><strong>Cost</strong></td>\n<td>Low</td>\n<td>High</td>\n<td>Medium-High</td>\n</tr>\n<tr>\n<td><strong>Use Case</strong></td>\n<td>Simple FAQ, Lookup</td>\n<td>Complex Research, Workflows</td>\n<td>High-Accuracy Q&amp;A</td>\n</tr>\n<tr>\n<td><strong>Reliability</strong></td>\n<td>Moderate</td>\n<td>High (Reasoning)</td>\n<td>Very High (Grounding)</td>\n</tr>\n<tr>\n<td><strong>Implementation</strong></td>\n<td>Easy</td>\n<td>Complex</td>\n<td>Moderate</td>\n</tr>\n</tbody>\n</table>\n<h3>A sensible path</h3>\n<p>For most startups, the most logical path is to start with Traditional RAG and then add a Corrective layer. You ship quickly and still have a roadmap for more reliability as your dataset grows.</p>\n\nBefore building a full CRAG loop, add one cheap relevance check: ask a small model \"does this context answer the question? yes/no\" before generating. Return \"I don't know\" on a no, and count how often it fires.\n\n<h2>Implementation in production: Laravel and cloud</h2>\n<p>Building these architectures takes more than an LLM API key. It needs a solid backend to handle state, queues, and data processing.</p>\n<p>In a Laravel environment, you can run the agentic loops with job queues and dedicated service classes. Laravel's ecosystem suits the orchestrator logic that drives an agentic RAG system.</p>\n<pre><code>// Conceptual Laravel Service for a Corrective RAG Flow\nclass RagOrchestrator {\n    public function handleQuery(string $userQuery) {\n        // 1. Retrieve initial chunks\n        $chunks = $this-&gt;vectorStore-&gt;search($userQuery);\n\n        // 2. Evaluate with a Critic\n        $evaluation = $this-&gt;critic-&gt;evaluate($userQuery, $chunks);\n\n        if ($evaluation-&gt;isIrrelevant()) {\n            // 3. Corrective action: Web Search or Retry\n            $chunks = $this-&gt;webSearch-&gt;search($userQuery);\n        }\n\n        // 4. Final generation\n        return $this-&gt;llm-&gt;generate($userQuery, $chunks);\n    }\n}\n</code></pre>\n<h3>Scale the pieces separately</h3>\n<p>When deploying these systems, Docker and cloud infrastructure management matter a lot. You need to scale your vector database and your orchestration layer independently.</p>\n<p>Tools like Coolify for self-hosting, or managed GCP services, help with the compute demands of multi-step agentic loops.</p>\n\nCache agent sub-query results in Redis, keyed by a hash of the normalized sub-query, with a short TTL. Track the hit rate for a week to see how much repeated work your loops were doing.\n\n<h2>Match the architecture to the task</h2>\n<ul>\n<li><strong>Traditional RAG</strong> is for speed and simplicity. It is the baseline for all projects.</li>\n<li><strong>Agentic RAG</strong> is for thinking tasks. Use it when the user needs a consultant, not just a search bar.</li>\n<li><strong>Corrective RAG</strong> is for truth tasks. Use it when hallucinations are a business risk.</li>\n<li><strong>Caching</strong> is non-negotiable in agentic RAG. Cache the results of sub-queries to cut cost and latency for recurring questions.</li>\n</ul>\n\nRAG is no longer \"one size fits all.\" The move toward Agentic and Corrective systems shifts the focus of AI engineering to reliability, so pick the architecture that gives your users answers instead of noise.\n<ul>\n<li>Start with Traditional RAG to validate your data and basic retrieval quality.</li>\n<li>Implement a Critic layer (Corrective RAG) early if accuracy is your primary KPI.</li>\n<li>Reserve Agentic RAG for workflows that require actual decision-making and tool use.</li>\n<li>Monitor your retrieval hit rate and token usage religiously.</li>\n<li>Use a modular backend structure like Laravel to manage the complexity of multi-loop architectures.\n</li></ul>\n\n<p>If you're deciding which RAG architecture fits your product, <a href=\"https://ansezz.com/services/\">here's how I help teams ship it</a>.</p>\n<p>Which architecture currently provides the best balance of cost and accuracy for your production workloads?</p>\n</figure></figure>",
      "date_published": "2026-06-11T00:00:00.000Z",
      "date_modified": "2026-06-11T00:00:00.000Z",
      "tags": [
        "ai",
        "rag",
        "agentic-ai",
        "llm",
        "vector-search",
        "ai-engineering"
      ],
      "image": "https://ansezz.com/blog/rag-architectures-traditional-agentic-corrective/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/monolith-vs-microservices/",
      "url": "https://ansezz.com/blog/monolith-vs-microservices/",
      "title": "Monolith vs microservices: how to choose",
      "summary": "Monolith vs microservices: the real technical trade-offs for Laravel and Shopify teams, when each pays off, and why a modular monolith often wins.",
      "content_html": "<p>Microservices are not the default goal. For most Laravel and Shopify teams, a well-structured monolith ships faster and costs less.</p>\n\n- A monolith is one deployable unit: simple deploys, shared memory, ACID data and easy end-to-end tests.\n- Microservices buy independent scaling, team autonomy and fault isolation, but charge a \"network tax\" of latency, partial failures and eventual consistency.\n- A modular monolith with strict internal boundaries is often the best middle ground, and it makes a later split much easier.\n\n<p>Complexity is the silent killer of engineering velocity. You start with a clean codebase and a clear vision.</p>\n<p>Within months, your deployment times have doubled and your team is tripping over each other's pull requests. You face the classic crossroads: monolith vs microservices.</p>\n<h3>The cost of choosing wrong</h3>\n<p>Choosing the wrong path early can lead to catastrophic technical debt, or operational overhead that drains your budget before you find product-market fit.</p>\n<h3>Resist the default</h3>\n<p>In modern web development, especially with Laravel or Shopify, the pressure to adopt microservices is huge. The industry often treats distributed systems as the default goal.</p>\n<p>For many businesses, a well-structured monolith is both the starting point and the most efficient way to scale. Here is the engineering behind both patterns, to help you decide for your next project on Google Cloud or AWS.</p>\n<h2>Defining the monolithic architecture</h2>\n<p>A monolithic architecture is a unified software model where all components are interconnected and interdependent.</p>\n<p>In Laravel, your routes, controllers, models and background jobs live in one repository. It is a single deployable unit: when you push code, the whole application is built and shipped to your server or container.</p>\n<h3>Why simple wins</h3>\n<p>Monoliths are often unfairly called \"legacy.\" In reality, they offer big advantages for rapid development.</p>\n<p>All modules share the same memory space and database, so communication between parts of the system is nearly instant. No network latency, API versioning or complex distributed transactions. For a Shopify app or a custom web solution, that simplicity means faster shipping.</p>\n<h3>The benefits of a single codebase</h3>\n<ol>\n<li><strong>Simplified deployment:</strong> you need only one CI/CD pipeline. Whether you use GitHub Actions to deploy to a <a href=\"https://ansezz.com/blog/coolify-docker-saas-hosting/\">Docker-based host</a> or a VPS, the process stays straightforward.</li>\n<li><strong>Cross-cutting concerns:</strong> authentication, logging and caching are easier when they are central. You don't replicate them across multiple services.</li>\n<li><strong>End-to-end testing:</strong> testing the whole user journey is simpler because you can run the full stack on one machine without complex orchestration.</li>\n</ol>\n<h2>The microservices shift</h2>\n<p>Microservices break the application into small, independent services that talk over a network. Each focuses on one business domain, such as billing, user management or inventory.</p>\n<p>They are often containerized with Docker and managed by orchestrators like Kubernetes or Google Kubernetes Engine (GKE).</p>\n<h3>Team autonomy</h3>\n<p>The main drivers are scalability and team autonomy. When your team grows beyond 15 or 20 developers, a monolith can become a bottleneck.</p>\n<p>Microservices let different squads own parts of the system. One team updates the billing service in Go while another updates the Shopify sync engine in PHP, and a bug in one service is less likely to take down the whole application.</p>\n<h3>When to consider microservices</h3>\n<ul>\n<li><strong>Independent scaling needs:</strong> if your Shopify webhook processing needs heavy CPU but your admin dashboard is lightly used, microservices let you scale only the heavy parts.</li>\n<li><strong>Technology diversity:</strong> you might need a Python service for AI-driven <a href=\"https://ansezz.com/blog/7-rag-mistakes-production/\">RAG systems</a> while keeping your core business logic in Laravel.</li>\n<li><strong>Fault isolation:</strong> a crash in the reporting service should not stop customers from checking out.</li>\n</ul>\n<figure>\n<h2>Monolith vs microservices: comparing the trade-offs</h2>\n<p>The choice is always a trade-off between simplicity and flexibility. There is no silver bullet: you weigh operational cost against developer experience.</p>\n<table>\n<thead>\n<tr>\n<th>Feature</th>\n<th>Monolith</th>\n<th>Microservices</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Operational complexity</strong></td>\n<td>Low</td>\n<td>High</td>\n</tr>\n<tr>\n<td><strong>Development speed</strong></td>\n<td>Fast (early stage)</td>\n<td>Slow (early stage)</td>\n</tr>\n<tr>\n<td><strong>Scalability</strong></td>\n<td>Vertical / horizontal (whole)</td>\n<td>Independent per service</td>\n</tr>\n<tr>\n<td><strong>Data consistency</strong></td>\n<td>Strong (ACID)</td>\n<td>Eventual consistency</td>\n</tr>\n<tr>\n<td><strong>Network latency</strong></td>\n<td>Minimal</td>\n<td>Significant (API hops)</td>\n</tr>\n<tr>\n<td><strong>Testing</strong></td>\n<td>Simple</td>\n<td>Complex distributed testing</td>\n</tr>\n</tbody>\n</table>\n<h3>The hidden cost of distribution</h3>\n<p>Microservices add a \"network tax.\" Every call from one service to another adds latency, and you must handle partial failures.</p>\n<p>If the user service is down, how does the order service react? Patterns like <a href=\"https://ansezz.com/blog/circuit-breakers-vector-db/\">circuit breakers</a> and retries become mandatory. That is code with nothing to do with your business logic: pure infrastructure overhead.</p>\n<p>Every service boundary you add is a network call that can fail and a contract you have to keep.</p>\n\nCount how many teams deploy your app independently today. If the answer is one, write down the specific pain a split would fix before you plan any services.\n\n<h2>Laravel and Shopify context</h2>\n<p>For most <a href=\"https://ansezz.com/blog/category/shopify/\">Shopify development</a> or <a href=\"https://ansezz.com/blog/category/laravel/\">Laravel projects</a>, the \"Majestic Monolith\" is the better choice.</p>\n<p>Shopify gives you a solid platform-as-a-service (PaaS) foundation. Your backend's main job is often to handle OAuth, process webhooks and manage a custom database.</p>\n<h3>The distributed monolith</h3>\n<p>Splitting a Shopify app into microservices too early often leads to \"distributed monolith\" syndrome: the complexity of microservices with components that are still tightly coupled.</p>\n<p>If you cannot deploy Service A without also deploying Service B, you have missed the benefits of the architecture. You have only added network latency and deployment pain.</p>\n<h2>Cloud strategy on GCP</h2>\n<p>Google Cloud Platform (GCP) has good tools for both patterns.</p>\n<h3>Monolith on Cloud Run</h3>\n<p>For a monolith, Cloud Run is a strong choice. It hides server management and scales your container based on request traffic. It is cost-effective because you only pay when your code is running.</p>\n<h3>Microservices with Pub/Sub</h3>\n<p>For microservices, you might use GKE or a set of Cloud Run services connected through <a href=\"https://ansezz.com/blog/event-driven-pubsub/\">Pub/Sub</a>. That gives you asynchronous communication.</p>\n<p>When a Shopify order is created, the \"Order Service\" publishes an event to Pub/Sub. The \"Shipping Service\" and \"Email Service\" both listen for it and act independently.</p>\n<p>This architecture is capable, but it needs real investment in observability tools like Cloud Logging and Cloud Trace to debug issues across service boundaries.</p>\n<h2>The hybrid approach: modular monolith</h2>\n<p>You don't have to choose between a \"big ball of mud\" and a complex mesh of services. The modular monolith is an effective middle ground.</p>\n<p>You keep a single codebase and database, but you strictly enforce boundaries inside the code.</p>\n<h3>Boundaries in Laravel</h3>\n<p>In Laravel, this means separate namespaces or even local packages for different domains. Your \"Billing\" code should not call the \"Inventory\" models directly. It should use internal interfaces or events.</p>\n<p>You get the deployment simplicity of a monolith while making it easy to extract a module into a standalone microservice later.</p>\n\n  <div>Billing queries Inventory tables directly, and any change can break any other part of the app.</div>\n  <div>Billing talks to Inventory through an interface or an event. One deploy, but each module can be pulled out later without a rewrite.</div>\n\n<figure>\n<h3>Defer the decision</h3>\n<p>I go deeper on the mechanics (contracts, schema-per-module and the friction signals that justify a split) in <a href=\"https://ansezz.com/blog/modular-monolith-first/\">modular monoliths first</a>. \"Deferred decision making\" is often the wisest move in software engineering.</p>\n<p>When the time comes to carve a module out, my guide on going <a href=\"https://ansezz.com/blog/monolith-to-microservices/\">from monolith to microservices</a> walks through the strangler-fig migration step by step.</p>\n\nPick two domains in your Laravel app (for example Billing and Inventory) and search for direct model calls between them. Replace one with an interface or event this week.\n\n\n- **Start with a monolith.** Unless you are building for a large organization with multiple teams, microservice overhead will slow you down.\n- **Enforce boundaries early.** A modular structure inside your monolith prevents spaghetti code and keeps future scaling possible.\n- **Use managed services.** GCP Cloud Run for hosting and Cloud SQL for your database keep the DevOps burden low.\n- **Scale components before splitting apps.** Use queues (Laravel Horizon) and background workers for heavy tasks before moving to full microservices.\n- **Monitor and trace.** Whatever the architecture, invest in central logging and performance monitoring to find bottlenecks before users feel them.\n\n<p>If you're weighing a split, <a href=\"https://ansezz.com/services/\">here's how I help Laravel and Shopify teams choose between a modular monolith and services</a>.</p>\n<p>Are you finding that your current monolithic deployment is hitting performance ceilings that vertical scaling can no longer solve?</p>\n</figure></figure>",
      "date_published": "2026-06-10T00:00:00.000Z",
      "date_modified": "2026-06-10T00:00:00.000Z",
      "tags": [
        "architecture",
        "devops",
        "laravel",
        "shopify",
        "infrastructure",
        "cloud-platforms"
      ],
      "image": "https://ansezz.com/blog/monolith-vs-microservices/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/prompt-engineering-vs-context-engineering/",
      "url": "https://ansezz.com/blog/prompt-engineering-vs-context-engineering/",
      "title": "Prompt engineering vs context engineering",
      "summary": "The shift from instruction design to data infrastructure: how context engineering uses RAG, MCP and memory to build accurate AI systems.",
      "content_html": "<p>Prompt engineering is about how you ask. Context engineering is about what the model can see when you ask. Past a certain point, only the second one moves accuracy.</p>\n\n- Prompts hit an \"instruction ceiling\": no phrasing fixes a model that lacks the data, and crowded context windows lose details in the middle.\n- Context engineering builds the system around the model: retrieval (RAG), tools (MCP), memory and workflow state.\n- In practice that means a context layer in your backend that assembles the right documents, history and tools before the model sees the user's input.\n\n<p>You spent three hours tweaking a system prompt so your LLM would stop inventing details about customer orders. It worked for ten minutes.</p>\n<p>Then a user asked about a return policy from 2024, and the model confidently described a 90-day cash-back guarantee that doesn't exist.</p>\n<h3>The instruction ceiling</h3>\n<p>You've hit the instruction ceiling. No amount of \"You are a helpful assistant\" or \"Think step by step\" lets a model reason its way out of missing data.</p>\n<p>You're treating a data problem as a language problem. As we move from simple chatbots to agentic systems, the focus shifts from how we talk to models to how we feed them.</p>\n<h2>Where prompts stop working</h2>\n<p>Prompt engineering was the first discipline of the AI era. You craft instructions, examples and formatting rules to steer a Large Language Model (LLM), and you might spend days testing phrasing or adding few-shot examples.</p>\n<p>That works well for creative writing and basic text transformation.</p>\n<h3>Stateless and crowded</h3>\n<p>Prompts are stateless and limited. You squeeze logic, data and constraints into a finite context window, and when it gets crowded the model loses track of details.</p>\n<p>Researchers call this <a href=\"https://arxiv.org/abs/2307.03172\">\"lost in the middle\"</a>: models use information at the start and end of a long context better than information buried in the middle. Relying only on prompts also means relying on training data, which is static and often outdated.</p>\n<h3>Static instructions in production</h3>\n<p>In production, especially in <a href=\"https://ansezz.com/blog/agentic-commerce-shopify/\">Shopify development</a> or custom enterprise software, static instructions are dangerous. If the model doesn't know real-time stock levels or the specs of a new product, clever phrasing won't prevent a made-up answer.</p>\n<h2>What is context engineering?</h2>\n<p>Context engineering is the programmatic management of everything around the model. It covers the text you send, and also the architecture that decides which data, tools and memories the model can reach at any moment.</p>\n<p>If prompt engineering is writing a script for an actor, context engineering is building the stage, supplying the props and hiring the research team that feeds facts into the actor's earpiece.</p>\n<h3>The moving parts</h3>\n<ul>\n<li><strong>Retrieval pipelines:</strong> fetch relevant data from external sources on demand.</li>\n<li><strong>Tool governance:</strong> decide which APIs or functions the model can call.</li>\n<li><strong>Memory management:</strong> store and retrieve past interactions for continuity.</li>\n<li><strong>State orchestration:</strong> track where the user is in a workflow or business process.</li>\n</ul>\n<p>Moving from \"telling\" to \"showing\" lowers the load on the model. Instead of asking it to remember every policy, you build a system that finds the one relevant policy and puts it in front of the model.</p>\n<figure>\n<h2>RAG: the context delivery engine</h2>\n<p>Retrieval-augmented generation (RAG) is the most common form of context engineering. It closes the knowledge gap by connecting your LLM to a vector store: Postgres with the <a href=\"https://ansezz.com/blog/picking-the-right-rag-stack/\">pgvector extension</a>, or a dedicated database like Pinecone, Weaviate or Qdrant.</p>\n<p>When a user asks a question, the system first converts it into an embedding (a numeric representation of meaning). It searches for the most similar documents, then injects those snippets into the prompt.</p>\n\n  <div>\"Answer questions based on our manual.\" The model guesses which part of the manual applies, or invents one.</div>\n  <div>\"Here are the three paragraphs from our manual that answer this question. Use them.\" The model works from the actual source.</div>\n\n<p>That change cuts the chance of the model making things up. RAG has its own pitfalls, though, and it's worth knowing the common <a href=\"https://ansezz.com/blog/7-rag-mistakes-production/\">RAG mistakes in production</a>.</p>\n<h2>MCP: the shared data plumbing</h2>\n<p>The <strong>Model Context Protocol (MCP)</strong> is one of the most useful pieces of context engineering. Anthropic introduced it in November 2024 and donated it in December 2025 to the Agentic AI Foundation, a Linux Foundation fund co-founded by Anthropic, Block and OpenAI.</p>\n<p>Say you're building an agent that checks GitHub issues, looks up a customer in a CRM and writes a technical summary. Traditionally you'd write custom integration code for each source. With MCP, servers expose those sources in a standard format any MCP client can discover and call.</p>\n<h3>Pull, don't push</h3>\n<p>MCP lets the model pull context when it needs it, so it explores your data instead of passively receiving one big prompt. It turns context from a static block of text into a queryable interface, and it's a core part of a modern <a href=\"https://ansezz.com/blog/api-gateway-ai-stack/\">API gateway for AI stacks</a>.</p>\n<table>\n<thead>\n<tr>\n<th>Feature</th>\n<th>Prompt engineering</th>\n<th>Context engineering</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Focus</strong></td>\n<td>Phrasing and tone</td>\n<td>Data and tools</td>\n</tr>\n<tr>\n<td><strong>Tooling</strong></td>\n<td>Text editors, prompt playgrounds</td>\n<td>Vector DBs, MCP, RAG pipelines</td>\n</tr>\n<tr>\n<td><strong>State</strong></td>\n<td>Mostly stateless</td>\n<td>Stateful and persistent</td>\n</tr>\n<tr>\n<td><strong>Scalability</strong></td>\n<td>Hard to maintain as docs grow</td>\n<td>Built for large document sets</td>\n</tr>\n<tr>\n<td><strong>Primary goal</strong></td>\n<td>Better behavior</td>\n<td>Better accuracy</td>\n</tr>\n</tbody>\n</table>\n<p>A model can only be as right as the context you hand it.</p>\n\nTake your longest system prompt and highlight every fact in it (policies, prices, product details). Move each one into a retrieval source or a tool, and keep only behavior rules in the prompt.\n\n<h2>Memory and state</h2>\n<p>Prompting focuses on the \"now.\" Context engineering also handles the \"always\": long-term memory.</p>\n<p>In a Laravel application, you might keep user preferences or past interactions in a database or Redis. When the user returns, the context layer retrieves those fragments and summarizes them for the model.</p>\n<h3>More than the last five messages</h3>\n<ol>\n<li><strong>Summarization:</strong> condense long histories into short summaries. My <a href=\"https://ansezz.com/tools/token-counter/\">token counter</a> shows how much a summary saves.</li>\n<li><strong>Metadata filtering:</strong> pull only memories relevant to the current topic.</li>\n<li><strong>Hierarchy:</strong> decide which memories are \"core\" and which are \"ephemeral.\"</li>\n</ol>\n<h2>Agents vs chatbots</h2>\n<p>Context is what separates a basic chatbot from an AI agent. A chatbot waits for a prompt and responds. An agent works inside a context: tools (via MCP), knowledge (via RAG) and history (via memory stores).</p>\n<p>With a context-first mindset, you spend less time in a prompt playground and more time building reliable data connectors. You care about the <strong>quality of your data</strong> more than the <strong>cleverness of your adjectives</strong>.</p>\n<h3>A context layer in Laravel</h3>\n<p>For Laravel and Vue.js stacks, this means a context layer in your backend that gathers everything the AI needs before it sees the user's input.</p>\n<pre><code>// A simple context orchestrator in Laravel\nclass AIContextManager\n{\n    public function buildContext(User $user, string $query): array\n    {\n        $knowledge = $this-&gt;vectorStore-&gt;search($query);\n        $history = $this-&gt;memory-&gt;getRecent($user-&gt;id);\n        $tools = $this-&gt;mcp-&gt;getAvailableTools(['inventory', 'shipping']);\n\n        return [\n            'system_prompt' =&gt; view('prompts.system')-&gt;render(),\n            'retrieved_docs' =&gt; $knowledge,\n            'user_history' =&gt; $history,\n            'available_tools' =&gt; $tools,\n            'user_query' =&gt; $query,\n        ];\n    }\n}\n</code></pre>\n<figure>\n\nLog the full context your app sends for ten real requests. For each one, mark which pieces the answer actually used. Anything that is never used is a candidate to drop or retrieve on demand.\n\n\n- **Prompts are instructions; context is knowledge.** Stop trying to teach the model your whole business inside a system prompt.\n- **Invest in RAG early.** Postgres with pgvector gives you a scalable way to handle growing documentation.\n- **Adopt MCP for tool calling.** It standardizes how your AI reaches your APIs and databases.\n- **Manage state outside the prompt.** Let your backend (Laravel, Node or similar) handle memory and session state.\n- **Focus on data quality.** A perfect RAG system with bad data still gives bad answers. Context engineering is data engineering at heart.\n\n<p>If you're building that context layer for production, <a href=\"https://ansezz.com/services/\">here's how I help teams design RAG, MCP and memory for their AI features</a>.</p>\n<p>How are you balancing long system prompts against external retrieval in your current AI work?</p>\n</figure></figure>",
      "date_published": "2026-06-10T00:00:00.000Z",
      "date_modified": "2026-06-10T00:00:00.000Z",
      "tags": [
        "ai",
        "ai",
        "rag",
        "mcp",
        "vector-search",
        "laravel",
        "devops"
      ],
      "image": "https://ansezz.com/blog/prompt-engineering-vs-context-engineering/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/ml-engineer-vs-ai-engineer/",
      "url": "https://ansezz.com/blog/ml-engineer-vs-ai-engineer/",
      "title": "ML engineer vs AI engineer",
      "summary": "ML engineer vs AI engineer: who trains the model, who orchestrates the system, their diverging toolsets, and which role your AI roadmap actually needs.",
      "content_html": "<p>An ML engineer builds the model. An AI engineer builds the product around a model someone else trained. Hire the wrong one and nothing ships.</p>\n\n- ML engineers train and tune models: data pipelines, feature stores, experiments and metrics like F1 score and loss curves.\n- AI engineers orchestrate foundation models through APIs: RAG, agents, MCP, guardrails, latency and cost.\n- Hire an ML engineer when no existing model fits your problem, and an AI engineer when a smart assistant with your data will do.\n\n<p>You are ready to ship AI features, but your hiring roadmap is a mess of overlapping buzzwords and conflicting job descriptions.</p>\n<p>Hiring a researcher to build a production chatbot often means a month of experiments with zero shipped code. Asking a standard full-stack developer to optimize a vector search index usually ends in a high-latency disaster.</p>\n<h3>A wider gap than it looks</h3>\n<p>The gap between training a model and orchestrating a system is wider than most founders realize.</p>\n<p>The choice between an ML engineer and an AI engineer decides whether you build a proprietary brain or a fast application powered by existing intelligence.</p>\n<h2>The model scientist: defining the ML engineer</h2>\n<p>The machine learning engineer lives in the world of weights, biases and data distributions. Their main goal is to create, train or fine-tune models that solve specific predictive or generative tasks.</p>\n<p>They build the logic that lives inside the API, instead of calling someone else's.</p>\n<h3>A day in the life</h3>\n<p>An ML engineer might spend the day in PyTorch or TensorFlow designing a custom architecture. They handle the \"dirty work\" of data: cleaning large datasets, managing feature stores and dealing with data drift.</p>\n<p>Their mental model is rooted in applied statistics and experimental iteration. If you need a model that predicts fraudulent transactions from proprietary financial data, you need an ML engineer. They make sure the model generalizes well and doesn't overfit the training set.</p>\n<h3>Their stack</h3>\n<p>Their stack sits at the heavy end of the data world: Spark for data processing, Weights &amp; Biases for experiment tracking and NVIDIA GPUs for heavy lifting.</p>\n<p>The output is a serialized model file or a dedicated inference service that other systems call. This is a deep-tech role focused on the \"how\" of intelligence, and the split between <a href=\"https://ansezz.com/blog/training-vs-inference/\">training and inference</a> shapes most of their day.</p>\n<h2>The systems architect: defining the AI engineer</h2>\n<p>The AI engineer is a fairly new kind of developer. They treat the model as a black-box service.</p>\n<p>Their focus is the infrastructure around the LLM that makes it useful for users, rather than training it. They are the bridge between raw intelligence and a working product.</p>\n<h3>A day in the life</h3>\n<p>An AI engineer spends their time on system design and software engineering. They work with foundation models like Claude, GPT and Gemini through APIs, and focus on RAG (retrieval-augmented generation) and agentic workflows.</p>\n<p>Instead of tweaking hyperparameters, they tweak system prompts and design tool-use schemas.</p>\n<h3>Owners of the AI user experience</h3>\n<p>The AI engineer owns the user experience of AI. That includes managing latency, adding guardrails and making sure the output is structured correctly for the frontend.</p>\n<p>If you are building a custom support bot on <a href=\"https://ansezz.com/blog/agentic-commerce-shopify/\">Shopify</a>, you want an AI engineer. They know how to connect the store's data to the LLM and handle multi-turn conversations.</p>\n<p>One role decides what the model knows. The other decides what users get from it.</p>\n<figure>\n<h2>Technical stack: a side-by-side comparison</h2>\n<p>To see the differences, look at the tools used in 2026. There is overlap, but the main focus areas are distinct.</p>\n<table>\n<thead>\n<tr>\n<th>Feature</th>\n<th>ML Engineer</th>\n<th>AI Engineer</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Core Models</strong></td>\n<td>Custom PyTorch/TensorFlow, XGBoost</td>\n<td>Claude, GPT, Llama, Gemini APIs</td>\n</tr>\n<tr>\n<td><strong>Primary Task</strong></td>\n<td>Training, Fine-tuning, Evaluation</td>\n<td>Prompt Engineering, RAG, Agents</td>\n</tr>\n<tr>\n<td><strong>Data Focus</strong></td>\n<td>Feature engineering, labeling, drift</td>\n<td>Chunking, indexing, retrieval quality</td>\n</tr>\n<tr>\n<td><strong>Programming</strong></td>\n<td>Python (heavy), C++, CUDA</td>\n<td>Python, TypeScript, PHP</td>\n</tr>\n<tr>\n<td><strong>Infrastructure</strong></td>\n<td>Kubernetes, GPUs, Feature Stores</td>\n<td>Vector DBs (pgvector), MCP, Serverless</td>\n</tr>\n<tr>\n<td><strong>Success Metric</strong></td>\n<td>F1 Score, Accuracy, Loss curves</td>\n<td>Latency, Hallucination rate, User NPS</td>\n</tr>\n</tbody>\n</table>\n<h3>Two pipelines</h3>\n<p>The ML engineer owns the pipeline from raw data to a deployed model. The AI engineer owns the pipeline from a user query to a relevant, cited response.</p>\n<p>One builds the engine; the other builds the car and picks the right fuel.</p>\n<h2>The RAG bridge: where the roles meet</h2>\n<p>Retrieval-augmented generation (RAG) is the most common meeting point for these two roles. A solid RAG system needs high-quality embeddings and a fast, scalable vector database.</p>\n<h3>The ML side</h3>\n<p>An ML engineer might train a domain-specific embedding model or a custom reranker to improve search relevance. They look at the mathematical similarity between vectors and tune the distance functions.</p>\n<h3>The AI side</h3>\n<p>The AI engineer usually builds the end-to-end RAG system. They decide how to chunk documents, how to manage metadata and how to run <a href=\"https://ansezz.com/blog/vector-search-vs-graph-search/\">hybrid search</a> with tools like pgvector or Pinecone.</p>\n<p>They also handle context management: making sure the LLM gets exactly what it needs without exceeding its context window or blowing the budget. If you are just starting, avoid common <a href=\"https://ansezz.com/blog/7-rag-mistakes-production/\">RAG mistakes in production</a> by focusing on retrieval quality before model size.</p>\n\nBefore you consider a bigger model or a custom embedding, build a set of 20 real questions with known answers and measure how often your retrieval returns the right chunk.\n\n<h2>Agentic systems: the new frontier</h2>\n<p>In 2026, the focus has shifted from simple chatbots to agentic systems: AI agents that use tools, browse the web and run code to finish complex tasks.</p>\n<h3>AI engineers drive it</h3>\n<p>AI engineers are the main drivers of this shift. They use frameworks like LangGraph or the Claude Agent SDK to build multi-step workflows.</p>\n<p>They implement the <a href=\"https://ansezz.com/blog/claude-mcp-dev-tools/\">Model Context Protocol (MCP)</a> to give agents access to internal databases, local files and external APIs. That takes deep software engineering skills, because agents need reliable error handling and human-in-the-loop checkpoints to be useful in a business.</p>\n<h3>ML engineers supply the specialists</h3>\n<p>ML engineers support this by building the specialized tools agents call. An agent might call a custom fraud-detection model built by an ML engineer as part of its reasoning loop.</p>\n<p>The AI engineer orchestrates the logic, while the ML engineer provides the specialized predictions.</p>\n\n  <div>Collect data, train, evaluate on F1 and loss, ship a model file or inference service.</div>\n  <div>Take a user query, retrieve context, call a model and tools (maybe the ML engineer's model), check the output, return a cited answer.</div>\n\n<h2>DevOps and deployment: from MLOps to LLMOps</h2>\n<p>The deployment cycle looks very different for each role.</p>\n<h3>MLOps</h3>\n<p>ML engineers deal with MLOps. See <a href=\"https://ansezz.com/blog/devops-vs-mlops/\">DevOps vs MLOps</a> for how that discipline differs from classic ops.</p>\n<p>It means specialized infrastructure for model training, managing GPU clusters and watching for model drift over time. They often use Docker and Kubernetes so their models can scale to millions of inference requests.</p>\n<h3>LLMOps</h3>\n<p>AI engineers focus on LLMOps, or \"AI DevOps.\" Their concerns are API reliability, cost management and caching.</p>\n<p>They need their AI apps to stay fast and to swap a model version (for example, moving a Claude or GPT model to a newer release) without breaking the system. Tools like Coolify or cloud setups on Google Cloud are common here for hosting the middleware that connects the LLM to the world.</p>\n<figure>\n<h2>How to choose the right path for your project</h2>\n<p>The decision depends on the \"intelligence\" you need.</p>\n<h3>When you need an ML engineer</h3>\n<p>If your problem is unique and no pre-trained model can solve it, you need an ML engineer. That is true for niche medical imaging, high-frequency trading or specialized sensor data analysis.</p>\n<h3>When you need an AI engineer</h3>\n<p>If a very smart assistant with access to your company data can solve it, you need an AI engineer. Most modern software falls into this group.</p>\n<p>You don't need to train a new model to build an AI-powered project management tool. You need a great system around a foundation model. This often comes down to a <a href=\"https://ansezz.com/blog/rag-vs-fine-tuning/\">RAG vs fine-tuning</a> decision, and the answer is usually RAG.</p>\n<h3>The new full-stack</h3>\n<p><a href=\"https://ansezz.com/blog/ai-vs-traditional-development/\">AI vs traditional development</a> is becoming less about the code and more about data orchestration. The AI engineer is a full-stack developer who has learned to manage non-deterministic outputs.</p>\n\nWrite one sentence describing the AI feature on your roadmap. If it needs a model that doesn't exist yet, start with an ML engineer; if it needs an existing model wired to your data, start with an AI engineer.\n\n\n- **ML engineers** are model builders focused on training, data science and applied statistics.\n- **AI engineers** are system builders focused on app development, RAG, agents and model orchestration.\n- **The overlap** is Python and data basics, but the daily toolsets are moving apart.\n- **RAG** is the main point where both roles add to one production feature.\n- **Agents** are where AI engineering is heading, and they need strong backend logic and API integration skills.\n- **Choosing the right role** avoids wasted budget and gets your AI features into production.\n\n<p>If you need the AI engineering side (RAG, agents and MCP wired into your product), <a href=\"https://ansezz.com/services/\">here's how I help teams ship production AI features</a>.</p>\n<p>When you look at your roadmap, is the main bottleneck the lack of a custom model, or making an existing model work reliably with your data?</p>\n</figure></figure>",
      "date_published": "2026-06-09T00:00:00.000Z",
      "date_modified": "2026-06-09T00:00:00.000Z",
      "tags": [
        "ai",
        "ai",
        "machine-learning",
        "llm",
        "rag",
        "agentic-ai"
      ],
      "image": "https://ansezz.com/blog/ml-engineer-vs-ai-engineer/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/load-balancer-vs-reverse-proxy/",
      "url": "https://ansezz.com/blog/load-balancer-vs-reverse-proxy/",
      "title": "Load balancer vs reverse proxy: scale vs security",
      "summary": "Reverse proxies handle SSL, caching, and security; load balancers handle scale and availability. The differences, and how to layer both in a Laravel stack.",
      "content_html": "<p>A reverse proxy decides how a request is handled. A load balancer decides where it goes. Most production stacks need both.</p>\n\n- A reverse proxy shields your app: it terminates SSL, caches responses, routes by path and hides your backend servers.\n- A load balancer spreads traffic across a pool of identical servers with round robin, least connections or IP hash, and drops unhealthy nodes.\n- In production you layer them: a load balancer at the edge, Nginx on each node, and your Laravel app behind it.\n\n<p>Your server is gasping for air. A traffic spike from a marketing campaign or a product launch on Shopify has pushed your single Laravel instance to its limit.</p>\n<p>CPU is pinned at 99 percent, requests are timing out and the database is struggling with the number of open connections. You know you need to scale and put a buffer between the internet and your app.</p>\n<h3>Two terms, two jobs</h3>\n<p>When you look at architecture diagrams, you see two terms used almost interchangeably: load balancer and reverse proxy.</p>\n<p>Choosing the wrong one, or missing how they work together, leads to \"Frankenstein\" architectures. You might add layers that add latency without adding value, or leave your app exposed to risks a proper proxy would have blocked.</p>\n<p>To build a solid <a href=\"https://ansezz.com/blog/category/laravel/\">Laravel</a> application or a fast <a href=\"https://ansezz.com/blog/category/shopify/\">Shopify</a> app, you need to know how these two components differ.</p>\n<h2>The gatekeeper: understanding the reverse proxy</h2>\n<p>A reverse proxy is a server that sits in front of one or more web servers and intercepts client requests. It acts as a shield and a middleman.</p>\n<p>Users talk to the reverse proxy first. It decides how to handle the request before passing it to your backend Laravel or Node.js application.</p>\n<p>If you are fuzzy on which direction it faces, see <a href=\"https://ansezz.com/blog/forward-proxy-vs-reverse-proxy/\">forward proxy vs reverse proxy</a>. They sit on opposite ends of the connection.</p>\n<h3>Nginx does the dirty work</h3>\n<p>In the world of <a href=\"https://ansezz.com/blog/coolify-self-hosted-saas/\">Coolify and self-hosted SaaS</a>, Nginx is the most common reverse proxy.</p>\n<p>Instead of exposing your application server directly to the internet, the reverse proxy handles the \"dirty work\" of HTTP communication.</p>\n<h3>Key functions of a reverse proxy</h3>\n<ul>\n<li><strong>SSL termination:</strong> encrypting and decrypting HTTPS traffic is CPU-intensive. The proxy handles the certificates and takes this work off your app server, so your Laravel workers focus on business logic instead of handshakes.</li>\n<li><strong>Caching:</strong> a reverse proxy can store copies of static assets or even dynamic responses. When a second user requests the same data, the proxy serves it from cache instead of hitting your app. (Nginx, for example, keeps cached responses on disk with the keys held in shared memory.) This cuts backend load a lot.</li>\n<li><strong>Request routing:</strong> you can route by URL path. For example, send every request starting with <code>/api</code> to one service and everything else to a frontend app.</li>\n<li><strong>Security and anonymity:</strong> hiding your backend IP addresses makes it much harder for attackers to target your servers directly. A proxy can also act as a basic Web Application Firewall (WAF) to block malicious traffic patterns.</li>\n</ul>\n<figure>\n\nCheck whether your app server is reachable directly by its public IP. If it is, put Nginx (or your platform's proxy) in front and close the app port to the outside.\n\n<h2>The traffic controller: understanding the load balancer</h2>\n<p>A load balancer's mission is high availability and <a href=\"https://ansezz.com/blog/horizontal-vs-vertical-scaling/\">horizontal scaling</a>.</p>\n<p>If you have five identical Laravel servers running in a <a href=\"https://ansezz.com/blog/coolify-docker-saas-hosting/\">Docker</a> cluster, the load balancer makes sure no single server gets overwhelmed while others sit idle.</p>\n<h3>Layer 4 vs layer 7</h3>\n<p>Layer 4 load balancers work at the transport level (TCP/UDP). They decide based on IP addresses and ports without looking at the request content.</p>\n<p>Layer 7 load balancers work at the application level (HTTP/HTTPS). They can route on headers, cookies or URL parameters.</p>\n<h3>How load balancers distribute work</h3>\n<p>Load balancers use algorithms to decide which server gets the next request:</p>\n<ol>\n<li><strong>Round robin:</strong> requests go to servers in a loop. It is simple but assumes all your backend servers have the same capacity.</li>\n<li><strong>Least connections:</strong> the load balancer tracks active requests per server and sends new traffic to the least busy one.</li>\n<li><strong>IP hash:</strong> the client's IP picks the server, so a user stays on the same server. That matters for apps that store session data locally instead of in Redis.</li>\n</ol>\n<p>The proxy cares about each request. The balancer cares about the whole pool.</p>\n<h2>Technical nuances: comparing the two</h2>\n<p>The confusion often comes from tools like Nginx, HAProxy and Traefik that can do both jobs. The difference in concept still matters for system design.</p>\n<table>\n<thead>\n<tr>\n<th>Feature</th>\n<th>Reverse Proxy</th>\n<th>Load Balancer</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Primary Goal</strong></td>\n<td>Security, routing, and efficiency</td>\n<td>Availability and throughput</td>\n</tr>\n<tr>\n<td><strong>Backend Pattern</strong></td>\n<td>Usually one logical service</td>\n<td>A pool of identical nodes</td>\n</tr>\n<tr>\n<td><strong>Caching</strong></td>\n<td>Excellent support for static + dynamic</td>\n<td>Usually minimal or none</td>\n</tr>\n<tr>\n<td><strong>Health Checks</strong></td>\n<td>Basic (is the backend up?)</td>\n<td>Advanced (latency, error rates, load)</td>\n</tr>\n<tr>\n<td><strong>OSI Layer</strong></td>\n<td>Mostly Layer 7 (Application)</td>\n<td>Layer 4 (Transport) or Layer 7</td>\n</tr>\n</tbody>\n</table>\n<h3>Same family, different focus</h3>\n<p>Both are reverse-proxy roles wearing different hats. A load balancer focuses on <em>distribution</em> (spreading traffic across a pool). A reverse proxy focuses on <em>transformation</em> (SSL, caching, routing, security).</p>\n<p>The same tool (Nginx, HAProxy, Traefik) can do both. In a full <a href=\"https://ansezz.com/blog/api-gateway-ai-stack/\">API gateway</a>, these roles merge into a single entry point that manages both security and distribution across your microservices.</p>\n<h2>Real-world implementation in Laravel and Shopify</h2>\n<p>When building custom web solutions, you rarely pick only one. You layer them. A production Laravel app usually looks like a \"sandwich\" of these components.</p>\n<h3>The Laravel DevOps stack</h3>\n<p>In a typical cloud setup on GCP or AWS, the request flow looks like this:</p>\n<ol>\n<li><strong>The cloud load balancer:</strong> your public entry point. It receives traffic and spreads it across virtual machines or Kubernetes nodes.</li>\n<li><strong>The local reverse proxy (Nginx):</strong> each node runs Nginx. It terminates SSL, serves static CSS and JS from disk and forwards PHP requests to PHP-FPM.</li>\n<li><strong>The application (Laravel):</strong> Laravel receives the cleaned-up request from Nginx.</li>\n</ol>\n<p>This gives you redundancy. If one Nginx instance fails, the cloud load balancer sees it through a health check and stops sending traffic to that node, so users stay served.</p>\n\n  <div>Laravel handles SSL, static files and every request on one box. When it fails or fills up, the whole site goes down.</div>\n  <div>A cloud load balancer health-checks several nodes. Nginx on each node handles SSL and static files, and Laravel only runs business logic.</div>\n\n<h3>The Shopify app context</h3>\n<p>Shopify apps have their own needs. During big events like Black Friday, webhook traffic can spike hard and in bursts.</p>\n<p>Shopify gives you only five seconds to respond to each delivery before it counts as failed. It retries failed deliveries 8 times over the next 4 hours, and after 8 consecutive failures a subscription created through the Admin API is deleted. You cannot ride that out on a single server.</p>\n<h3>Acknowledge fast, process later</h3>\n<p>Use a load balancer to take in these webhooks and spread them across a fleet of worker nodes. Acknowledge fast and process the payload <a href=\"https://ansezz.com/blog/synchronous-vs-asynchronous-communication/\">asynchronously</a>.</p>\n<p>A reverse proxy at the edge can add rate limiting. That stops a single store from hogging your app's resources and keeps your <a href=\"https://ansezz.com/blog/agentic-commerce-shopify/\">agentic commerce systems</a> responsive for all merchants.</p>\n<figure>\n\nTime your Shopify webhook handler end to end. If it does real work before returning 200, move that work to a queue job and return right away.\n\n<h2>Why \"both\" is usually the answer</h2>\n<p>Modern web development has moved away from the \"one server\" model. Even for small startups, a managed load balancer costs little compared to downtime.</p>\n<p>A reverse proxy like Nginx or Traefik is standard practice because it keeps your application code simple. You don't want SSL handling logic inside your Laravel controllers.</p>\n<h3>Blue-green deployments</h3>\n<p>Combining a load balancer and a reverse proxy lets you run blue-green deployments. You spin up a new version, test it, then switch the load balancer from the old version to the new one.</p>\n<p>If something breaks, you flip the switch back. You need both components working together to get this level of control.</p>\n\nAdd a health check endpoint (for example `/up`) to your Laravel app and point your load balancer's health check at it, so a broken node leaves the rotation on its own.\n\n\n- **Use a reverse proxy** if you have a single server but need SSL, caching and basic security.\n- **Add a load balancer** as soon as you need to scale across multiple servers or need high availability.\n- **Use Nginx or HAProxy** to fill both roles in one software layer for small and medium projects.\n- **Offload SSL termination** to the proxy layer to keep your Laravel or Node.js app responsive.\n- **Set up health checks** in your load balancer to remove unhealthy servers from the rotation automatically.\n- **Choose layer 7 balancing** if you need to route on HTTP headers or URL paths.\n\n<p>If you're putting a proxy and load balancer in front of a Laravel or Shopify app, <a href=\"https://ansezz.com/services/\">here's how I help teams set up SSL, caching and scaling layers for production</a>.</p>\n<p>What is your current bottleneck: scaling the number of concurrent connections, or managing the complexity of your request routing?</p>\n</figure></figure>",
      "date_published": "2026-06-08T00:00:00.000Z",
      "date_modified": "2026-06-08T00:00:00.000Z",
      "tags": [
        "architecture",
        "devops",
        "laravel",
        "shopify",
        "infrastructure",
        "networking",
        "scaling"
      ],
      "image": "https://ansezz.com/blog/load-balancer-vs-reverse-proxy/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/logging-vs-monitoring/",
      "url": "https://ansezz.com/blog/logging-vs-monitoring/",
      "title": "Logging vs monitoring: a guide for scaling",
      "summary": "Monitoring tells you a system is unhealthy; logging tells you why. How to combine metrics, structured logs, and correlation IDs for fast debugging.",
      "content_html": "<p>Your dashboard shows a latency spike. It can't tell you why the database connection is timing out. That gap is the difference between monitoring and logging.</p>\n\n- Monitoring tracks numbers over time and tells you the system is unhealthy. Logging records events and tells you why.\n- Use structured JSON logs with a `request_id` so you can search and trace one request across your whole stack.\n- Link the two with correlation IDs, alert on user-focused SLOs, and turn frequent log patterns into metrics.\n\n<p>Your production environment is failing and customers are reporting 500 errors. You check your dashboard and see a latency spike, but the graph doesn't say why.</p>\n<p>You dive into your text files for the stack trace, but the logs are unstructured and impossible to search during a crisis.</p>\n<h3>Two pillars, often mixed up</h3>\n<p>Engineering teams often blur these two pillars of observability. The result is slow incident response and fragile systems.</p>\n<p>Picking tools is only part of it. You need a clear strategy for your <a href=\"https://ansezz.com/blog/category/devops/\">DevOps solutions</a> that keeps availability high and debugging fast. One is the pulse of your system. The other is the forensic record.</p>\n<h2>Logging vs monitoring: what vs why</h2>\n<p>Monitoring tracks metrics over time to understand the state of a system. It answers \"Is the system healthy?\" by looking at numbers.</p>\n<p>You monitor CPU usage, memory and request latency. When a metric crosses a threshold, your monitoring system fires an alert.</p>\n<h3>Logs tell the story</h3>\n<p>Logging records discrete events inside an application or infrastructure. It answers \"What exactly happened?\" by giving context.</p>\n<p>A log entry might hold a user ID, a timestamp and the error message from a failed <a href=\"https://ansezz.com/blog/category/laravel/\">Laravel</a> job. Logs are granular and tell the story of a single request through your stack.</p>\n<table>\n<thead>\n<tr>\n<th>Feature</th>\n<th>Monitoring</th>\n<th>Logging</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Data type</strong></td>\n<td>Metrics (numbers/counters)</td>\n<td>Events (text/structured data)</td>\n</tr>\n<tr>\n<td><strong>Purpose</strong></td>\n<td>Detection and health</td>\n<td>Diagnosis and forensics</td>\n</tr>\n<tr>\n<td><strong>Frequency</strong></td>\n<td>Aggregated over time</td>\n<td>Recorded per occurrence</td>\n</tr>\n<tr>\n<td><strong>Alerting</strong></td>\n<td>Threshold-based (e.g., CPU &gt; 80%)</td>\n<td>Event-based (e.g., Fatal Error)</td>\n</tr>\n<tr>\n<td><strong>Storage</strong></td>\n<td>Time-series databases</td>\n<td>Log aggregators/search engines</td>\n</tr>\n</tbody>\n</table>\n<h2>Monitoring: the pulse of your infrastructure</h2>\n<p>Monitoring focuses on numbers. It is the first line of defense in any production environment.</p>\n<p>By watching trends, you can predict failures before they happen. A slow rise in memory use over 48 hours might point to a memory leak in a long-running Docker container.</p>\n<h3>The four golden signals</h3>\n<p>In a stack using tools like <a href=\"https://ansezz.com/blog/coolify-docker-saas-hosting/\">Coolify and Docker</a>, monitoring shows whether your containers perform as expected. Focus on the \"Four Golden Signals\":</p>\n<ul>\n<li><strong>Latency:</strong> the time it takes to serve a request.</li>\n<li><strong>Traffic:</strong> the demand placed on the system.</li>\n<li><strong>Errors:</strong> the rate of requests that fail.</li>\n<li><strong>Saturation:</strong> how \"full\" your service is.</li>\n</ul>\n<h3>Alert on what users feel</h3>\n<p>Good monitoring needs meaningful thresholds. An alert that fires every time CPU hits 70% is noise if your app is meant to be CPU-intensive.</p>\n<p>Define Service Level Objectives (SLOs) that match user experience. If users don't notice a 100ms delay, don't wake up an engineer for it.</p>\n\nPick one alert that fired last week without anyone needing to act. Rewrite it against a user-facing SLO (for example error rate or P95 latency) or delete it.\n\n<h2>Logging: the black box recorder</h2>\n<p>If monitoring tells you the plane is losing altitude, logging tells you why the engine stalled. Logs give you the evidence to debug complex issues.</p>\n<p>In a distributed system, one user action might touch several services. Without central logging, finding the root cause is like finding a needle in a haystack.</p>\n<h3>Structure your logs</h3>\n<p>A common mistake is using unstructured logs. Text lines like <code>[2026-06-21] Error: something went wrong</code> are hard for machines to parse.</p>\n<p>Modern teams use structured logging, formatting each log as a JSON object. You can then filter and query on fields like <code>user_id</code>, <code>request_id</code> or <code>environment</code>. When a log line won't parse, my <a href=\"https://ansezz.com/tools/json-formatter/\">JSON formatter</a> points to the exact character.</p>\n<pre><code>{\n  \"timestamp\": \"2026-06-21T14:30:05Z\",\n  \"level\": \"error\",\n  \"message\": \"Payment gateway timeout\",\n  \"context\": {\n    \"user_id\": 4502,\n    \"order_id\": \"ORD-9921\",\n    \"gateway\": \"stripe\",\n    \"attempt\": 3\n  },\n  \"request_id\": \"req-a1b2c3d4\"\n}\n</code></pre>\n<h3>Trace one request everywhere</h3>\n<p>Including a <code>request_id</code> in every log entry lets you trace a single request across your infrastructure. This matters a lot in <a href=\"https://ansezz.com/blog/category/shopify/\">Shopify development</a>, where webhooks and API calls often chain together.</p>\n<p>A dashboard tells you something broke. A searchable log tells you what to fix.</p>\n<figure>\n\n  <div>`Error: something went wrong`. No user, no order, no request ID. You grep text files and hope.</div>\n  <div>A JSON entry with `level`, `user_id`, `order_id` and `request_id`. One query finds every line for that request.</div>\n\n<h2>Observability in the Laravel ecosystem</h2>\n<p>Laravel has a capable logging system built on the Monolog library. Out of the box, it supports channels such as single files, daily rotated files, syslog and Slack, plus <code>stack</code> channels that send one message to several channels at once.</p>\n<h3>Ship logs off the server</h3>\n<p>In production, configure Laravel to ship logs to a central service. Sending them to a service like Logstash, Sentry or Datadog means you don't lose data when a server instance is terminated.</p>\n<h3>Watch your queues</h3>\n<p>Monitoring a Laravel app means more than checking that the web server is up. Watch the health of your background queues too.</p>\n<p>If your Redis queue backs up, customers might not get order confirmation emails or account activation links. Tools like Laravel Pulse or Horizon give real-time monitoring for these app-level metrics.</p>\n<h3>Close the loop</h3>\n<p>A common pattern is to log exceptions to a dedicated tracker while watching the error rate on a dashboard. A spike in the \"Error Rate\" metric prompts you to check the \"Exception Logs\" for the actual stack trace.</p>\n\nAdd a middleware that sets a `request_id` with `Log::withContext()` at the start of each request, so every log line in that request carries it.\n\n<h2>Scaling Shopify apps with metrics and logs</h2>\n<p>High-scale Shopify apps need their own approach to logging and monitoring. They live at the mercy of external API <a href=\"https://ansezz.com/blog/rate-limiting-vs-throttling/\">rate limits</a> and webhook delivery speed.</p>\n<p>If your app handles thousands of webhooks per minute, you can't log every successful event to a text file. You will quickly run out of disk space.</p>\n<h3>Monitor the limits, log the failures</h3>\n<p>Instead, use monitoring to track the \"Webhook Success Rate\" and your API rate-limit use. Shopify's GraphQL Admin API meters calls by calculated query cost using a leaky bucket (on the Standard plan, your quota restores at 100 points per second, and a single query can't cost more than 1,000 points). The REST Admin API uses a request-based bucket. To see how fast a full sync can run inside those limits, use my <a href=\"https://ansezz.com/tools/shopify-rate-limit-planner/\">Shopify rate limit planner</a>.</p>\n<p>Watch how close you run to those ceilings, then use logging to investigate the specific failed payloads when the success rate drops.</p>\n<h3>Agents need logs too</h3>\n<p>This matters even more for <a href=\"https://ansezz.com/blog/agentic-commerce-shopify/\">agentic commerce</a> apps where AI agents act on behalf of a merchant. If an agent fails to update an inventory count, you need the specific log to understand the logic failure.</p>\n<p>For Shopify Plus merchants, performance matters a lot. Monitoring the \"Time to First Byte\" (TTFB) of your app's embedded components makes sure you aren't slowing down the merchant's admin.</p>\n<h2>The DevOps synergy: linking logs and metrics</h2>\n<p>Mature engineering teams don't treat logging and monitoring as separate silos. They link them.</p>\n<p>When an alert fires in your monitoring tool, it should link straight to the logs for that timeframe and service.</p>\n<h3>Correlation IDs</h3>\n<p>This is often done with \"Correlation IDs.\" When a request enters your system, you give it a unique ID.</p>\n<p>That ID is attached to every log entry and trace written during the request. When a metric spikes, you filter the logs for that time window and follow the IDs to the requests that caused it.</p>\n<h3>Monitor your logs</h3>\n<p>You should also monitor your logs. Counting how often certain log patterns appear gives you new metrics.</p>\n<p>If \"Database connection lost\" shows up more than five times in a minute, turn that log event into a metric that triggers a high-priority alert.</p>\n<figure>\n\nPick one error message that always means trouble (for example \"Database connection lost\") and create a log-based metric with an alert when it appears more than five times a minute.\n\n<h2>Best practices for technical teams</h2>\n<ol>\n<li><strong>Automate everything:</strong> your monitoring agents and logging drivers belong in your base server image or Dockerfile.</li>\n<li><strong>Log levels matter:</strong> use <code>debug</code> for local development, <code>info</code> for general production events, and <code>error</code> or <code>critical</code> for things that need a human.</li>\n<li><strong>Protect PII:</strong> never log personal data like passwords, credit card numbers or auth tokens. Use log masking to strip sensitive data.</li>\n<li><strong>Set retention policies:</strong> metrics are small and can be kept for months. Logs are large and expensive, so balance cost against your need for history.</li>\n<li><strong>Monitor the monitors:</strong> make sure your monitoring system itself is healthy. A silent monitoring system is a dangerous liability.</li>\n</ol>\n\n- Monitoring detects that a problem exists. Logging explains why it happened.\n- Metrics are numbers. Logs are detailed event records.\n- Use structured JSON logging to make your data searchable and machine-readable.\n- In Laravel, watch your queues so background tasks keep completing.\n- In Shopify apps, track API rate limits as a primary health metric.\n- Link logs and metrics with correlation IDs to cut Mean Time to Resolution (MTTR).\n- Base monitoring on user-focused SLOs to avoid alert fatigue.\n\n<p>If you're building observability for a Laravel or Shopify app, <a href=\"https://ansezz.com/services/\">here's how I help teams set up structured logs, SLO alerts and correlation IDs</a>.</p>\n<p>How do you tell a noisy alert from a real system failure in your current production stack?</p>\n</figure></figure>",
      "date_published": "2026-06-08T00:00:00.000Z",
      "date_modified": "2026-06-08T00:00:00.000Z",
      "tags": [
        "devops",
        "observability",
        "laravel",
        "shopify",
        "infrastructure"
      ],
      "image": "https://ansezz.com/blog/logging-vs-monitoring/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/load-balancer-vs-api-gateway/",
      "url": "https://ansezz.com/blog/load-balancer-vs-api-gateway/",
      "title": "Load balancer vs API gateway",
      "summary": "Load balancers distribute traffic; API gateways enforce policy. The real differences, when to use each, and how to layer both in a production stack.",
      "content_html": "<p>Two boxes sit at the front of almost every production system. One spreads traffic so nothing falls over. The other decides who gets in and where they go.</p>\n\n- A load balancer spreads traffic across identical backends at L4 or L7, and stays fast because it ignores who is calling.\n- An API gateway enforces policy at the edge: auth, rate limits, transformations, caching and routing by intent to the right service.\n- Mature stacks run both: a load balancer at the edge, a gateway behind it, and internal load balancers in front of each service.\n\n<p>Engineers constantly confuse these two components. They look similar on an architecture diagram, but they solve very different problems.</p>\n<h3>Picking the wrong one costs you</h3>\n<p>Pick the wrong one and you either bolt fragile business logic onto a component built to be dumb and fast, or you pay for an expensive policy engine to do a job simple round-robin could handle.</p>\n<p>The load balancer vs API gateway decision shapes how you scale, secure and observe everything behind it. Here is what each one does, where they differ and why mature systems almost always run both.</p>\n<h2>What a load balancer actually does</h2>\n<p>A load balancer has one job: take incoming traffic and spread it across a pool of identical backends so no single server gets overwhelmed.</p>\n<p>It is the component that lets you run three copies of your app instead of one and survive a traffic spike.</p>\n<h3>L4 vs L7</h3>\n<p>Load balancers work at one of two layers. An <strong>L4 (transport layer)</strong> balancer routes by IP and port. It does not look inside the request and just forwards TCP/UDP packets, which makes it extremely fast.</p>\n<p>An <strong>L7 (application layer)</strong> balancer understands HTTP. It can route based on the path or host header and terminate SSL/TLS. Algorithms like round-robin, least-connections and IP-hash decide which backend gets the next request.</p>\n<h3>Simple on purpose</h3>\n<p>A load balancer is largely <strong>stateless and content-agnostic</strong>. It cares about availability and distribution, not about who you are or whether you are allowed to make this call.</p>\n<p>That ignorance is a feature: it keeps the component simple, fast and reliable. If you have wondered how this differs from a reverse proxy, the two overlap heavily. See <a href=\"https://ansezz.com/blog/load-balancer-vs-reverse-proxy/\">load balancer vs reverse proxy</a> for where they diverge.</p>\n<h2>What an API gateway actually does</h2>\n<p>An API gateway is the opposite kind of component. It is opinionated, aware and full of business rules. It sits in front of your services as a single, managed front door for every API call.</p>\n<h3>Cross-cutting concerns in one place</h3>\n<p>A gateway handles the <strong>cross-cutting concerns</strong> you don't want duplicated in every microservice:</p>\n<ul>\n<li>Authentication and JWT validation</li>\n<li><a href=\"https://ansezz.com/blog/rate-limiting-ai-wallet/\">Rate limiting and quota enforcement</a></li>\n<li>Request/response transformation</li>\n<li>Protocol translation (for example, exposing internal <a href=\"https://ansezz.com/blog/rest-vs-grpc/\">gRPC services</a> as REST/JSON)</li>\n<li>Caching and per-client logging</li>\n</ul>\n<p>Instead of each service re-implementing auth, the gateway enforces it once at the edge.</p>\n<h3>Routing by intent</h3>\n<p>A gateway routes by <strong>intent</strong> as well as availability. It maps <code>/orders</code> to the orders service and <code>/users</code> to the users service, applies the right policy to each and stitches a fleet of microservices into one coherent API.</p>\n<p>That makes it a natural fit once you <a href=\"https://ansezz.com/blog/monolith-vs-microservices/\">move from a monolith to microservices</a> and need one stable front door over many services. It is also the base of a clean <a href=\"https://ansezz.com/blog/api-gateway-ai-stack/\">API gateway for an AI stack</a>, where the gateway governs which model endpoints and tools a request can reach.</p>\n<p>The load balancer asks \"is a server free?\" The gateway asks \"should this request get in at all?\"</p>\n<figure>\n<h2>Key differences at a glance</h2>\n<p>Both sit at the front and both can route HTTP, but their purpose, intelligence and state are very different.</p>\n<table>\n<thead>\n<tr>\n<th>Aspect</th>\n<th>Load Balancer</th>\n<th>API Gateway</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Primary job</strong></td>\n<td>Distribute traffic across servers</td>\n<td>Enforce policy + route by intent</td>\n</tr>\n<tr>\n<td><strong>Layer</strong></td>\n<td>L4 (TCP/UDP) or L7 (HTTP)</td>\n<td>L7 (HTTP/API aware)</td>\n</tr>\n<tr>\n<td><strong>Awareness</strong></td>\n<td>Content-agnostic</td>\n<td>Inspects auth, headers, payload</td>\n</tr>\n<tr>\n<td><strong>Core features</strong></td>\n<td>Health checks, SSL, algorithms</td>\n<td>Auth, rate limiting, caching, transforms</td>\n</tr>\n<tr>\n<td><strong>State</strong></td>\n<td>Mostly stateless</td>\n<td>Tracks clients, quotas, sessions</td>\n</tr>\n<tr>\n<td><strong>Optimizes for</strong></td>\n<td>Availability + throughput</td>\n<td>Security + control</td>\n</tr>\n</tbody>\n</table>\n<p>The rule of thumb: reach for a <strong>load balancer</strong> when you need raw distribution and uptime, and an <strong>API gateway</strong> when you need to apply rules (who can call this, how often and in what shape).</p>\n<h2>The layered architecture: using them together</h2>\n<p>In production, it is common to see both components working together. Each layer does a different job: one spreads the traffic, the other decides what gets through.</p>\n<h3>A typical request flow</h3>\n<ol>\n<li><strong>The L4/L7 load balancer:</strong> sits at the very edge of your network. It handles global traffic distribution, terminates SSL/TLS and passes clean HTTP traffic to the API gateway.</li>\n<li><strong>The API gateway:</strong> receives traffic from the load balancer. It checks the user's JWT, confirms they haven't exceeded their rate limit and routes the request to the correct microservice.</li>\n<li><strong>The backend services:</strong> these often have their own internal load balancers. For example, your Laravel API might run three pods, and a simple internal load balancer (often built into Docker or Kubernetes) spreads the gateway's traffic across them.</li>\n</ol>\n<h3>Scale each layer on its own</h3>\n<p>This separation of concerns lets you scale your infrastructure independently of your business rules.</p>\n<p>You can rewrite authentication logic in the gateway without touching load balancer configuration, and you can <a href=\"https://ansezz.com/blog/horizontal-vs-vertical-scaling/\">add or remove backend replicas</a> without the gateway noticing.</p>\n\n  <div>Each microservice validates JWTs and counts requests its own way. A policy change means editing and redeploying every service.</div>\n  <div>The gateway validates JWTs and enforces rate limits once at the edge. Services trust the gateway and focus on their own work.</div>\n\n\nList every place your services check auth or rate limits today. If the same logic lives in more than one service, that logic belongs in a gateway.\n\n<h2>Implementing traffic control with Laravel and Docker</h2>\n<p>When developing with Laravel, you often don't need a dedicated hardware appliance. You can build these patterns with open-source software like Nginx, Traefik or Kong.</p>\n<p>Using <a href=\"https://ansezz.com/blog/coolify-docker-saas-hosting/\">Docker containerization</a> makes this setup very portable.</p>\n<h3>Traefik labels as a gateway</h3>\n<p>If you use Traefik as an API gateway in a Docker Compose file, you can define your routing and middleware directly in the labels. That keeps your configuration close to your code.</p>\n<pre><code>services:\n  laravel-api:\n    image: your-org/laravel-app:latest\n    labels:\n      - \"traefik.http.routers.api.rule=Host(`api.example.com`)\"\n      - \"traefik.http.middlewares.api-auth.forwardauth.address=http://auth-service\"\n      - \"traefik.http.middlewares.rate-limit.ratelimit.average=100\"\n      - \"traefik.http.routers.api.middlewares=api-auth,rate-limit\"\n</code></pre>\n<p>Here, Traefik plays the API gateway role: it checks with an external auth service and enforces rate limits before the request ever reaches the Laravel application.</p>\n\nAdd a `ratelimit` middleware to one Traefik router in staging, hit it with a quick load test and confirm you get HTTP 429 responses once you pass the limit.\n\n<h2>Shopify and the gateway pattern</h2>\n<p>For Shopify developers, the API gateway pattern is very useful for \"headless\" storefronts. You talk to both the Shopify Storefront API and your own custom backend, and a gateway can unify these separate sources.</p>\n<p>It can cache frequent Shopify queries to reduce API consumption and speed things up for the end user.</p>\n<h3>Logic Shopify doesn't offer</h3>\n<p>A gateway also lets you add logic Shopify doesn't support natively: transforming payloads or merging multiple sources into one response, which cuts the number of round trips the browser makes.</p>\n<p>That pattern is a foundation for building <a href=\"https://ansezz.com/blog/agentic-workflows-vibe-coding/\">agentic workflows</a> in modern commerce.</p>\n<figure>\n\n- **Choose a load balancer** for simple traffic distribution, high throughput and maximum availability at the network layer.\n- **Choose an API gateway** for complex routing, security enforcement, protocol translation and a unified entry point to microservices.\n- **Layer your stack** with an L4/L7 load balancer at the edge and an API gateway behind it for business logic.\n- **Use automation** with tools like Docker and Coolify to manage these components without manual server configuration.\n- **Focus on observability** by using the gateway's per-client metrics for debugging and billing.\n\n<p>If you're untangling the front door of your production stack, <a href=\"https://ansezz.com/services/\">here's how I help teams design load balancer and API gateway layers</a>.</p>\n<p>How do you handle cross-cutting concerns like rate limiting and authentication across your current microservice fleet?</p>\n</figure></figure>",
      "date_published": "2026-06-07T00:00:00.000Z",
      "date_modified": "2026-06-07T00:00:00.000Z",
      "tags": [
        "architecture",
        "api-design",
        "networking",
        "architecture",
        "microservices",
        "devops",
        "laravel"
      ],
      "image": "https://ansezz.com/blog/load-balancer-vs-api-gateway/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/horizontal-vs-vertical-scaling/",
      "url": "https://ansezz.com/blog/horizontal-vs-vertical-scaling/",
      "title": "Horizontal vs vertical scaling",
      "summary": "Learn the key differences between horizontal and vertical scaling. Discover the right architectural strategy for scaling your web application effectively.",
      "content_html": "<p>Your database is at 90% CPU and users feel the lag. The real question is whether you buy a bigger server or add more of them.</p>\n\n- Vertical scaling adds CPU and RAM to one server: no code changes, but a hard ceiling and a single point of failure.\n- Horizontal scaling adds servers behind a load balancer: high availability and elasticity, but your app must be stateless.\n- Most teams end up hybrid, scaling the app layer out while keeping the database on one strong primary with read replicas.\n\n<p>Your application is slowing down. Users report latency, your database is hitting 90% CPU usage and your background workers are lagging.</p>\n<p>You know you need more power. Choosing the wrong scaling path can lead to wasted budget, needless complexity or a hard ceiling on your growth.</p>\n<h3>Scaling is a trade-off</h3>\n<p>Adding hardware is only half of the job. The other half is understanding the trade-offs between vertical and horizontal strategies.</p>\n<p>If you wait too long to scale out, you risk a single point of failure. If you scale out too early, you drown in the complexity of distributed systems.</p>\n<h2>Vertical scaling: the \"bigger box\" strategy</h2>\n<p>Vertical scaling, or \"scaling up,\" means adding more power to your existing server. This usually means more CPU, RAM or storage on a single instance.</p>\n<p>In a cloud like Google Cloud or AWS, this is as simple as changing your machine type from a small instance to a high-memory or high-compute one.</p>\n<h3>The path of least resistance</h3>\n<p>The application still lives on one machine, so you don't need to change your code. There is no need for load balancers, service discovery or complex networking.</p>\n<p>Your <a href=\"https://ansezz.com/blog/category/laravel/\">Laravel</a> application or monolith works exactly as it did before, just faster.</p>\n<h3>The hard ceiling</h3>\n<p>Every hardware generation has a limit. Eventually the top machine size available is either too expensive or simply not enough.</p>\n<p>Vertical scaling also does not solve redundancy. If that one big server goes down, your entire system goes down with it.</p>\n\nBefore you buy a bigger box, run your numbers through my [server capacity calculator](/tools/capacity-calculator/) to see how many workers your traffic really needs.\n\n<h2>Horizontal scaling: building the \"fleet\"</h2>\n<p>Horizontal scaling, or \"scaling out,\" means adding more machines to your infrastructure. Instead of one giant server, you have a fleet of smaller servers working in parallel.</p>\n<p>A load balancer sits in front of this fleet and spreads incoming traffic across the available nodes.</p>\n<h3>Built for failure</h3>\n<p>This is the gold standard for high availability and modern <a href=\"https://ansezz.com/blog/coolify-docker-saas-hosting/\">cloud infrastructure</a>. When one server fails, the load balancer routes traffic to the healthy ones.</p>\n<p>It is also very elastic. With <a href=\"https://ansezz.com/blog/smart-auto-scaling-ai/\">auto-scaling</a>, you add nodes during a traffic spike and remove them when things quiet down, so you only pay for what you use.</p>\n<h3>The catch: state</h3>\n<p>Horizontal scaling requires architectural changes. Your application must be <a href=\"https://ansezz.com/blog/stateless-vs-stateful-apps/\">stateless</a>.</p>\n<p>If a user uploads a file to Server A, Server B must be able to access it. If you store session data in Server A's local memory, the user gets logged out when the next request hits Server B. Move state to shared databases or object storage.</p>\n<p>A fleet only helps when any server can answer any request.</p>\n<figure>\n<h2>Key differences at a glance</h2>\n<p>Your choice depends on your current stage and technical needs. Here is how the two compare on the engineering metrics that matter most.</p>\n<table>\n<thead>\n<tr>\n<th>Feature</th>\n<th>Vertical scaling (scale up)</th>\n<th>Horizontal scaling (scale out)</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Primary action</strong></td>\n<td>Add CPU/RAM to one node</td>\n<td>Add more nodes to the pool</td>\n</tr>\n<tr>\n<td><strong>Complexity</strong></td>\n<td>Low (no code changes)</td>\n<td>High (distributed system)</td>\n</tr>\n<tr>\n<td><strong>Fault tolerance</strong></td>\n<td>Low (single point of failure)</td>\n<td>High (redundancy by design)</td>\n</tr>\n<tr>\n<td><strong>Scalability limit</strong></td>\n<td>Hard limit (hardware ceiling)</td>\n<td>Virtually unlimited</td>\n</tr>\n<tr>\n<td><strong>Maintenance</strong></td>\n<td>Requires downtime for resizing</td>\n<td>Zero-downtime rolling updates</td>\n</tr>\n<tr>\n<td><strong>Cost</strong></td>\n<td>Expensive at the high end</td>\n<td>More efficient through auto-scaling</td>\n</tr>\n</tbody>\n</table>\n<h2>The challenge of distributed state</h2>\n<p>When you move to a horizontal model, the database becomes the main bottleneck.</p>\n<p>Scaling the application layer is fairly easy because app servers are usually stateless. Scaling a database is harder because data must stay consistent across multiple nodes. Every new app server also opens its own database connections, and my <a href=\"https://ansezz.com/tools/postgres-connection-calculator/\">Postgres connection pool calculator</a> shows when you will hit <code>max_connections</code>.</p>\n<h3>Start with read replicas</h3>\n<p>For many <a href=\"https://ansezz.com/blog/category/shopify/\">Shopify</a> apps or custom web platforms, the first step into horizontal scaling is adding <a href=\"https://ansezz.com/blog/replication-vs-backup-laravel/\">read replicas</a>.</p>\n<p>You keep one \"primary\" database for writes and multiple \"replica\" databases for reads. This takes the heavy read load off the main node.</p>\n<h3>Sharding comes last</h3>\n<p>If your data grows beyond what a single primary node can handle, you might look into sharding. That means splitting your data across different database clusters.</p>\n<p>It works, but it adds a lot of complexity to how you query and join data.</p>\n\nCheck what share of your database queries are reads. If it's most of them, point those reads at a replica before you think about sharding.\n\n<h2>Scaling in practice: Laravel and DevOps</h2>\n<p>In the Laravel ecosystem, scaling often starts with vertical upgrades. I have seen many projects stay on a single strong VPS for years.</p>\n<p>As traffic grows, the move to horizontal scaling usually follows a clear pattern:</p>\n<ol>\n<li><strong>Externalize sessions:</strong> move sessions from the <code>file</code> driver to <code>redis</code> or <code>database</code>.</li>\n<li><strong>Centralize files:</strong> move local storage to an S3-compatible object store.</li>\n<li><strong>Add a load balancer:</strong> use Nginx or a cloud-provider load balancer to distribute traffic.</li>\n<li><strong>Decouple workers:</strong> move queue workers to their own instances so they don't compete with the web server for resources.</li>\n</ol>\n<h3>Tooling helps</h3>\n<p>Tools like <a href=\"https://ansezz.com/blog/coolify-self-hosted-saas/\">Coolify</a> or Docker Swarm make managing multiple containers easier. They let you define your infrastructure as code, so spinning up new nodes is simple.</p>\n\n  <div>Sessions in local files, uploads on local disk, queue workers on the web server. A second server would break logins and lose files.</div>\n  <div>Sessions in Redis, files in S3-compatible storage, workers on their own instances, Nginx in front. Adding a node is a config change.</div>\n\n<figure>\n\nSwitch your session driver from `file` to `redis` in a staging environment and log in through two app instances. If you stay logged in, step one is done.\n\n<h2>How to choose your strategy</h2>\n<p>There is no one-size-fits-all answer. Your choice should balance cost, time and reliability.</p>\n<p><strong>Choose vertical scaling if:</strong></p>\n<ul>\n<li>You are in the early stages of a startup or MVP.</li>\n<li>Your traffic is predictable and stable.</li>\n<li>You have a small team with limited DevOps resources.</li>\n<li>Your application architecture is tightly coupled or legacy.</li>\n</ul>\n<p><strong>Choose horizontal scaling if:</strong></p>\n<ul>\n<li>You need 99.9% or higher availability.</li>\n<li>Your traffic is bursty or growing fast.</li>\n<li>You are building a cloud-native application from scratch.</li>\n<li>You want to avoid the \"all-or-nothing\" risk of a single server.</li>\n</ul>\n<h3>Most teams go hybrid</h3>\n<p>In reality, most modern architectures mix both. You might scale your database vertically as far as you can while scaling your application layer horizontally.</p>\n<p>That gives you the reliability of a fleet with the simplicity of a single data source.</p>\n\n- Vertical scaling makes a single server stronger through CPU and RAM upgrades.\n- Horizontal scaling adds more servers to a pool and uses a load balancer to manage traffic.\n- Vertical scaling is simpler and needs no code changes, but it has a performance ceiling and no redundancy.\n- Horizontal scaling offers high availability and elasticity, but it requires a stateless application.\n- For most web apps, scaling out starts by externalizing sessions, files and background jobs.\n- Databases are the hardest part to scale horizontally, often needing replicas or sharding.\n\n<p>If you're planning a scaling path for your stack, <a href=\"https://ansezz.com/services/\">here's how I help teams move from one server to a fleet without breaking sessions or data</a>.</p>\n<p>At what point does the cost of managing a distributed horizontal system outweigh the cost of simply buying a larger server for your specific workload?</p>\n</figure></figure>",
      "date_published": "2026-06-06T00:00:00.000Z",
      "date_modified": "2026-06-06T00:00:00.000Z",
      "tags": [
        "architecture",
        "scaling",
        "devops",
        "infrastructure",
        "laravel",
        "architecture"
      ],
      "image": "https://ansezz.com/blog/horizontal-vs-vertical-scaling/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/llm-vs-ai-agent/",
      "url": "https://ansezz.com/blog/llm-vs-ai-agent/",
      "title": "LLM vs AI agent: from prompts to action",
      "summary": "The architectural shift from LLMs to autonomous AI agents. How memory, tool-use, and planning turn a stateless model into a system that takes action.",
      "content_html": "<p>An LLM gives you great answers and zero execution. An agent wraps it in tools, memory and a loop so it can actually do the work.</p>\n\n- An LLM is a stateless next-token predictor: no memory between calls, a knowledge cutoff and no access to outside systems.\n- An agent runs the LLM inside a loop (think, act with a tool, observe the result) backed by memory, tools, planning and reasoning.\n- Use a plain LLM for simple, linear, low-latency text tasks, and an agent when the work needs several steps or external systems.\n\n<p>Most developers start their AI journey by sending a prompt to an API and waiting for a text response. That works for simple summaries or creative writing.</p>\n<p>It fails when the task needs real-world actions. A single chat completion cannot run a multi-step refund process in a Shopify store or navigate a complex database schema to build a report.</p>\n<h3>The architecture is the limit</h3>\n<p>What holds you back is the architecture around the model.</p>\n<p>Relying only on Large Language Models (LLMs) is like hiring a genius professor who has no hands, no memory of yesterday and no computer. You get great answers but nothing gets done.</p>\n<h3>Agents add the missing parts</h3>\n<p>AI agents wrap the LLM in tools, memory and planning logic. Moving from \"talking to a model\" to \"building an autonomous system\" is one of the main changes in how we architect software today.</p>\n<h2>The LLM as a prediction engine</h2>\n<p>A Large Language Model is a stateless next-token predictor. When you send a prompt, the model uses its training to calculate the most probable sequence of words to follow your input.</p>\n<p>It does not \"think\" in the human sense. It runs one forward pass per generated token through a very large neural network.</p>\n<h3>What a standalone LLM can't do</h3>\n<p>The core traits of a standalone LLM:</p>\n<ul>\n<li><strong>Statelessness:</strong> the model does not remember previous interactions unless you include them in the current prompt.</li>\n<li><strong>Knowledge cutoff:</strong> it only knows what was in its training set up to a certain date.</li>\n<li><strong>No external interaction:</strong> by default, it cannot check your email, query your production database or browse the web.</li>\n<li><strong>Single-turn logic:</strong> it produces one output for one input. Any multi-step reasoning must happen inside that single output.</li>\n</ul>\n<h3>When that's enough</h3>\n<p>For simple applications, this is fine. If you are building a basic <a href=\"https://ansezz.com/blog/api-gateway-ai-stack/\">API gateway for AI</a>, a thin wrapper around a model might be all you need.</p>\n<p>As soon as you need the system to take initiative, the LLM alone falls short.</p>\n<h2>Defining the AI agent: the reasoning loop</h2>\n<p>An AI agent is an autonomous system that uses an LLM as its central reasoning engine. Unlike a single LLM call, an agent works inside a loop.</p>\n<p>It observes the environment, thinks about what to do next, takes an action with a tool, and then observes the result to decide its next step.</p>\n<h3>Agents fix their own mistakes</h3>\n<p>This loop lets the agent correct itself. If a database query fails, a standalone LLM would simply report the failure in its final response.</p>\n<p>An agent sees the error, works out what went wrong and tries a different query. Interleaving a reasoning step with an action and then observing the result is the ReAct (Reasoning + Acting) approach introduced by Yao et al. in 2022.</p>\n<p>The model answers. The loop around it is what gets things done.</p>\n<figure>\n<h2>The four pillars of agency</h2>\n<p>To turn an LLM into an agent, you add four layers of infrastructure.</p>\n<h3>1. Memory</h3>\n<p>LLMs have a \"context window,\" but it is temporary and expensive. Agents use external memory systems to keep state across sessions.</p>\n<p>That includes short-term memory for the current task steps and long-term memory for user preferences or historical data. We often use vector databases like pgvector to store and retrieve these memories. The difference between the <a href=\"https://ansezz.com/blog/context-window-vs-memory/\">context window and persistent memory</a> is what separates a toy from a production agent.</p>\n<h3>2. Tools</h3>\n<p>Tools are the hands of the agent: external functions, APIs or scripts the agent can choose to run.</p>\n<p>When the agent sees it needs information it doesn't have, it generates a structured command (usually a JSON object that matches the tool's schema) to call a tool. This is what lets agents act on stacks like Laravel or <a href=\"https://ansezz.com/blog/agentic-commerce-shopify/\">a Shopify store</a>.</p>\n<h3>3. Planning</h3>\n<p>Complex goals need to be broken into smaller tasks. An agent uses the LLM to create a roadmap.</p>\n<p>For example, a high-level request like \"Audit our cloud spend\" gets split into sub-tasks: \"List all AWS instances,\" \"Query pricing API\" and \"Generate CSV report.\"</p>\n<h3>4. Reasoning</h3>\n<p>Reasoning governs how the agent uses the other three pillars. It is the control loop that keeps the system running until the goal is reached or a stopping condition is met.</p>\n\nTake one task you currently handle with a single prompt and write down its steps. If it has more than one step that touches an outside system, it is a candidate for an agent loop.\n\n<h2>Technical comparison</h2>\n<table>\n<thead>\n<tr>\n<th>Feature</th>\n<th>Large Language Model (LLM)</th>\n<th>AI agent</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Execution</strong></td>\n<td>Passive (prompt-response)</td>\n<td>Active (goal-oriented)</td>\n</tr>\n<tr>\n<td><strong>Persistence</strong></td>\n<td>None (stateless)</td>\n<td>Persistent (external memory)</td>\n</tr>\n<tr>\n<td><strong>Capabilities</strong></td>\n<td>Text generation and analysis</td>\n<td>Tool use, API calls, web browsing</td>\n</tr>\n<tr>\n<td><strong>Reasoning</strong></td>\n<td>One-shot internal logic</td>\n<td>Multi-step iterative loop</td>\n</tr>\n<tr>\n<td><strong>Connectivity</strong></td>\n<td>Isolated</td>\n<td>Integrated with external systems</td>\n</tr>\n<tr>\n<td><strong>Architecture</strong></td>\n<td>Model-centric</td>\n<td>System-centric</td>\n</tr>\n</tbody>\n</table>\n<h2>Transitioning from RAG to agentic workflows</h2>\n<p>Retrieval-Augmented Generation (RAG) was the first step toward more capable AI. In a standard RAG setup, you retrieve relevant documents from a vector store and stuff them into the LLM prompt. It is a linear process.</p>\n<h3>Agentic RAG</h3>\n<p>Agentic RAG goes further. Instead of a fixed retrieval step, the agent decides <em>when</em> to search, <em>what</em> keywords to use and <em>whether</em> the results were actually useful.</p>\n<p>If the first search results are poor, the agent refines its query and searches again. I cover this in more depth in <a href=\"https://ansezz.com/blog/rag-architectures-traditional-agentic-corrective/\">traditional vs agentic vs corrective RAG</a>.</p>\n\n  <div>Retrieve once, stuff the results into the prompt, answer. Poor results go straight into the answer.</div>\n  <div>The agent decides when and what to search, checks if the results help, and searches again with a better query if they don't.</div>\n\n<h3>Fewer bad retrievals</h3>\n<p>Take the <a href=\"https://ansezz.com/blog/7-rag-mistakes-production/\">7 common RAG mistakes</a>. An agentic approach can reduce problems like \"hallucinated retrieval\" by cross-referencing multiple sources or checking facts against a structured database.</p>\n<h2>Building agents in production</h2>\n<p>Building an agentic system takes more than an API key. You need a solid backend to manage state and run the tools.</p>\n<p>Frameworks like LangGraph and CrewAI are popular with Python developers. In the PHP ecosystem, you can build capable agentic backends with Laravel and tools like <a href=\"https://ansezz.com/blog/claude-mcp-dev-tools/\">Claude MCP</a>.</p>\n<h3>MCP for tools</h3>\n<p>The Model Context Protocol (MCP) is especially useful for agent development. It gives agents a standard way to connect to local and remote data sources.</p>\n<p>Instead of writing custom connectors for every tool, you can use MCP to give your agent direct access to your filesystem, GitHub repos or database schemas.</p>\n<pre><code>// Example of a tool definition in a Laravel-based agent\npublic function getTools(): array\n{\n    return [\n        [\n            'name' =&gt; 'query_shopify_orders',\n            'description' =&gt; 'Retrieves order details from Shopify using GraphQL.',\n            'parameters' =&gt; [\n                'type' =&gt; 'object',\n                'properties' =&gt; [\n                    'order_id' =&gt; ['type' =&gt; 'string'],\n                ],\n            ],\n        ],\n    ];\n}\n</code></pre>\n<h3>Clear tool definitions</h3>\n<p>Clear tool definitions tell the agent exactly what it can do. The LLM then acts as the router, deciding which tool to trigger based on the user's intent.</p>\n<figure>\n\nRewrite the `description` of each tool your agent has so it says what the tool does, when to use it and what it returns. Vague descriptions are a common reason agents pick the wrong tool.\n\n<h2>When to choose an agent over an LLM</h2>\n<p>Not every feature needs to be an agent. Agents are more complex to build, harder to test and can cost more because of multiple LLM calls.</p>\n<p><strong>Use a standalone LLM when:</strong></p>\n<ul>\n<li>You need immediate, low-latency text generation.</li>\n<li>The task is simple and doesn't need external data.</li>\n<li>The workflow is linear and predictable.</li>\n</ul>\n<p><strong>Use an AI agent when:</strong></p>\n<ul>\n<li>The task needs multiple steps or logic branches.</li>\n<li>The system must interact with external APIs or databases.</li>\n<li>The goal is open-ended (for example, \"Research this topic and find three competitors\").</li>\n<li>The system needs to learn and adapt over time using memory.</li>\n</ul>\n\n- **LLMs are the engine.** They provide the reasoning but need a system around them to do real work.\n- **Agency is architectural.** You build it by adding memory, tool use and planning layers to your model.\n- **Reasoning loops are key.** Observing and correcting actions is what makes agents autonomous.\n- **Start with RAG, then move to agents.** Agentic RAG is a natural next step for teams already using vector databases.\n- **Standardize your tools.** Protocols like MCP make it easier to give agents the data they need without custom boilerplate.\n\n<p>If you want help turning a prompt-response feature into an agent that takes action, <a href=\"https://ansezz.com/services/\">here's how I work with teams shipping agents with tools, memory and MCP</a>.</p>\n<p>Is your current AI implementation stuck in a passive prompt-response loop, or are you ready to build systems that actually take action?</p>\n</figure></figure>",
      "date_published": "2026-06-06T00:00:00.000Z",
      "date_modified": "2026-06-06T00:00:00.000Z",
      "tags": [
        "ai",
        "llm",
        "agentic-ai",
        "rag",
        "architecture",
        "mcp"
      ],
      "image": "https://ansezz.com/blog/llm-vs-ai-agent/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/devops-vs-mlops/",
      "url": "https://ansezz.com/blog/devops-vs-mlops/",
      "title": "DevOps vs MLOps: key technical differences for 2026",
      "summary": "The critical differences between DevOps and MLOps. How to automate software delivery and manage machine learning lifecycles for production.",
      "content_html": "<p>DevOps ships code that behaves the same every day. MLOps ships models that quietly get worse as the world changes.</p>\n\n- DevOps treats code as the main artifact and relies on deterministic builds, tests and deployments.\n- MLOps adds two moving parts, data and models, so it needs a third loop: Continuous Training (CT) on top of CI and CD.\n- MLOps monitoring tracks statistical health (data drift, concept drift, accuracy) as well as system health (latency, errors).\n\n<p>Shipping application code to production is difficult. Shipping a machine learning model that keeps performing accurately over time is much harder.</p>\n<p>You might have a perfectly automated pipeline that builds your Docker images and deploys them to a cluster in seconds.</p>\n<h3>Correct code, broken product</h3>\n<p>If your model starts making wrong predictions because consumer behavior changed, your automated deployment of \"correct\" code is shipping a broken product. That is the core friction between traditional software operations and machine learning operations.</p>\n<h3>What the comparison is really about</h3>\n<p>DevOps focuses on the reliability and speed of software delivery. It treats code as the primary artifact.</p>\n<p>MLOps takes the same principles and adds two volatile new variables: data and models. DevOps solves \"it works on my machine.\" MLOps solves \"it worked on last month's training data.\"</p>\n<p>Understanding this divide is essential for any team moving beyond simple heuristic-based software and into <a href=\"https://ansezz.com/blog/category/ai/\">AI engineering</a>.</p>\n<h2>The core philosophy of DevOps</h2>\n<p>DevOps is built on the <a href=\"https://ansezz.com/blog/ci-vs-cd/\">continuous integration and continuous delivery (CI/CD)</a> pipeline. The goal is to shorten the development life cycle and ship changes continuously with high software quality.</p>\n<h3>A deterministic process</h3>\n<p>In a standard DevOps environment, like one running a <a href=\"https://ansezz.com/blog/coolify-docker-saas-hosting/\">Laravel application on Docker</a>, the process is fairly deterministic.</p>\n<p>If the source code passes its unit tests, the integration tests succeed and the environment configuration is correct, the application should behave predictably in production.</p>\n<h3>Static artifacts</h3>\n<p>The artifacts are static. Once a binary or a container image is built, its internal logic does not change based on the data flowing through it.</p>\n<p>Technical teams use DevOps to manage:</p>\n<ul>\n<li><strong>Source code:</strong> versioning application logic in Git.</li>\n<li><strong>Build automation:</strong> compiling code and packaging it into artifacts.</li>\n<li><strong>Infrastructure as Code (IaC):</strong> using tools like Terraform or Pulumi to define servers and networks.</li>\n<li><strong>Testing:</strong> running automated suites to catch regressions in logic.</li>\n</ul>\n<h2>The emergence of MLOps</h2>\n<p>MLOps, or Machine Learning Operations, is a set of practices for deploying and maintaining machine learning models in production reliably and efficiently.</p>\n<p>Calling it \"DevOps for AI\" misses the point. It borrows the automation mindset, but the artifacts in MLOps are fundamentally different.</p>\n<h3>Three moving parts</h3>\n<p>A machine learning system has three distinct components: code, data and the model. In DevOps, you mostly care about the code.</p>\n<p>In MLOps, a change in the data can break the system even if the code is untouched. That brings a level of non-determinism traditional CI/CD pipelines are not designed to handle.</p>\n<h3>Data drift in the real world</h3>\n<p>If you are building <a href=\"https://ansezz.com/blog/agentic-commerce-shopify/\">agentic commerce solutions on Shopify</a>, your recommendation engine might fail without any bug in the Python script. The cause could be a shift in the distribution of customer purchases during a holiday sale.</p>\n<p>This is known as data drift, and it requires a lifecycle with continuous monitoring and retraining.</p>\n<p>The code didn't change. The world did, and the model never noticed.</p>\n<figure>\n<h2>Key technical differences at a glance</h2>\n<p>Here are the main technical distinctions between the two methodologies.</p>\n<table>\n<thead>\n<tr>\n<th>Feature</th>\n<th>DevOps</th>\n<th>MLOps</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Primary artifact</strong></td>\n<td>Source code and binaries</td>\n<td>Code, datasets, and model weights</td>\n</tr>\n<tr>\n<td><strong>Lifecycle</strong></td>\n<td>Build, test, deploy</td>\n<td>Data prep, train, evaluate, deploy, monitor</td>\n</tr>\n<tr>\n<td><strong>Testing</strong></td>\n<td>Unit and integration tests</td>\n<td>Model validation, accuracy metrics, bias checks</td>\n</tr>\n<tr>\n<td><strong>Deployment</strong></td>\n<td>Static (service/binary)</td>\n<td>Dynamic (model endpoint or batch job)</td>\n</tr>\n<tr>\n<td><strong>Monitoring</strong></td>\n<td>Latency, CPU, error rates</td>\n<td>Data drift, model decay, precision, recall</td>\n</tr>\n<tr>\n<td><strong>Feedback loop</strong></td>\n<td>Code bug fixes</td>\n<td>Continuous retraining (CT)</td>\n</tr>\n</tbody>\n</table>\n<h2>The CI/CD/CT pipeline</h2>\n<p>The main architectural difference is a third loop. DevOps relies on CI (Continuous Integration) and CD (Continuous Delivery). MLOps adds CT (Continuous Training).</p>\n<h3>Continuous Integration (CI) in MLOps</h3>\n<p>CI here tests the data as well as the code. You must validate the schema of incoming datasets and make sure feature engineering logic is the same in the training environment and in production.</p>\n<p>A mismatch leads to training-serving skew, a common reason for model failure.</p>\n<h3>Continuous Delivery (CD) in MLOps</h3>\n<p>CD covers the automated deployment of the model service. This often means deploying a prediction API or updating a weights file in a running inference engine.</p>\n<p>Because models are large and expensive to run, MLOps pipelines often use canary deployments or shadow testing to check performance on live data before a full rollout.</p>\n<h3>Continuous Training (CT)</h3>\n<p>This is the signature of MLOps. A CT pipeline triggers a retraining job when certain conditions are met, either on a schedule or when performance degrades.</p>\n<p>The pipeline pulls new data, runs the training script, evaluates the new model version against a benchmark and registers it in a model registry if it passes.</p>\n\n  <div>Commit, build, test, deploy. A new release only happens when someone changes the code.</div>\n  <div>Commit, build, test, deploy, plus validate data, retrain, evaluate and register. A new model can ship because the data changed.</div>\n\n\nAdd one data check to your existing CI: validate the schema of the training or feature dataset (column names, types, null rates) and fail the build when it changes unexpectedly.\n\n<h2>Monitoring and the problem of drift</h2>\n<p>In traditional <a href=\"https://ansezz.com/blog/category/devops/\">DevOps</a>, monitoring centers on system health. You watch memory usage, response times and HTTP 500 errors. If the server is up and the code runs, the system is usually considered \"healthy.\"</p>\n<h3>Healthy server, failing model</h3>\n<p>In MLOps, system health is only half the story. You also need to monitor statistical health.</p>\n<p>A model might return a \"200 OK\" status code with 50ms latency, but if its predictions are nonsensical, the system is failing.</p>\n<h3>Two kinds of drift</h3>\n<p>There are two main types of drift that MLOps engineers must track:</p>\n<ol>\n<li><strong>Data drift:</strong> the input data distribution changes. For example, a model trained on high-end fashion data might struggle if the store expands into budget streetwear.</li>\n<li><strong>Concept drift:</strong> the relationship between input and output changes. For instance, what counted as a \"normal\" credit card transaction in 2019 might look very different during a global supply chain crisis.</li>\n</ol>\n<h3>Close the feedback loop</h3>\n<p>Detecting drift requires careful logging of every prediction and its eventual outcome. That creates a feedback loop that informs the next training cycle, a step often missed in early-stage <a href=\"https://ansezz.com/blog/ai-vs-traditional-development/\">AI development</a>.</p>\n\nStart logging every prediction with its input features and a request ID, then join the real outcome back to it later. Without that table, you can't measure drift at all.\n\n<h2>Tooling and infrastructure</h2>\n<p>The toolsets for these two fields are moving apart. Docker and Kubernetes are shared, but the special needs of machine learning have created a new stack.</p>\n<h3>Standard DevOps tools</h3>\n<ul>\n<li><strong>GitHub Actions / GitLab CI:</strong> pipeline orchestration.</li>\n<li><strong>Docker:</strong> containerization.</li>\n<li><strong>Terraform:</strong> infrastructure provisioning.</li>\n<li><strong>Prometheus / Grafana:</strong> system monitoring.</li>\n</ul>\n<h3>What MLOps adds</h3>\n<ul>\n<li><strong>MLflow / Weights &amp; Biases:</strong> experiment tracking and model versioning.</li>\n<li><strong>Kubeflow / Vertex AI:</strong> ML-specific orchestration.</li>\n<li><strong>Feature stores:</strong> central repositories for pre-processed data features.</li>\n<li><strong>Evidently AI / Arize:</strong> statistical monitoring and drift detection.</li>\n</ul>\n<p>Building a bridge between these tools is the mark of a mature technical organization. You need the stability of DevOps to host the infrastructure and the flexibility of MLOps to manage the intelligence layer.</p>\n<figure>\n<h2>Bridging the gap in production</h2>\n<p>For teams running complex web applications, the goal is integration, not picking one side.</p>\n<p>A modern stack might use a Laravel backend for user management, hosted on a <a href=\"https://ansezz.com/blog/coolify-self-hosted-saas/\">Coolify-managed VPS</a>, while calling an MLOps-managed <a href=\"https://ansezz.com/blog/training-vs-inference/\">inference endpoint</a> for personalized content.</p>\n<h3>Version your data</h3>\n<p>One practical way to start is versioning your datasets. You would never deploy code without a commit hash, so never train a model without knowing exactly which version of the data was used.</p>\n<p>This makes results reproducible, which is the first step toward a working MLOps workflow.</p>\n<h3>Treat retrieval as infrastructure</h3>\n<p>The same rigor applies to retrieval systems. Treat vector databases and retrieval pipelines as managed infrastructure inside your DevOps workflow, not as ad-hoc scripts.</p>\n<p>That discipline alone heads off many of the <a href=\"https://ansezz.com/blog/7-rag-mistakes-production/\">RAG failures</a> teams hit in production.</p>\n\nTag your next training run with the dataset version or hash it used, and store that tag in the model registry entry. If you can't reproduce a model from its tag, fix that before adding more tooling.\n\n\n- **DevOps is about code reliability:** CI/CD pipelines, automated testing and deterministic deployments.\n- **MLOps is about model reliability:** data validation, experiment tracking and statistical monitoring.\n- **The CT loop is essential:** continuous training keeps models from going stale as real-world data changes.\n- **Monitoring must cover two layers:** system metrics (latency, errors) and model metrics (drift, accuracy).\n- **Tooling is specialized:** standard DevOps tools for hosting, MLOps tools for training and the registry.\n- **Data is an artifact:** version your training data with the same rigor you apply to source code.\n\n<p>If you're adding models to a product that already ships with CI/CD, <a href=\"https://ansezz.com/services/\">here's how I help teams build MLOps pipelines with drift monitoring and retraining</a>.</p>\n<p>How do you plan to handle the statistical monitoring of your models once they move from a static environment into the unpredictable flow of production data?</p>\n</figure></figure>",
      "date_published": "2026-06-05T00:00:00.000Z",
      "date_modified": "2026-06-05T00:00:00.000Z",
      "tags": [
        "devops",
        "devops",
        "machine-learning",
        "ai-engineering",
        "ci-cd"
      ],
      "image": "https://ansezz.com/blog/devops-vs-mlops/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/context-window-vs-memory/",
      "url": "https://ansezz.com/blog/context-window-vs-memory/",
      "title": "Context window vs memory: building AI that remembers",
      "summary": "Stop context-stuffing your LLM prompts. The difference between the context window and persistent memory, and how RAG + pgvector scale AI agents affordably.",
      "content_html": "<p>A context window is a desk. Memory is a library. Stop trying to fit the whole library on the desk.</p>\n\n- The context window is the model's short-term working memory: big, temporary and paid for on every request.\n- Long-term memory lives outside the model, usually in a vector database such as pgvector, and RAG pulls only the relevant chunks into the window.\n- Production agents use tiers: recent messages in context, episodic and semantic memory through RAG, and tools (often via MCP) to fetch the rest on demand.\n\n<p>Large language models have a memory problem that costs you money and performance. You spend thousands of tokens feeding the same documentation, user history and context into every prompt.</p>\n<p>Yet the moment the session ends, the model forgets everything. It is like hiring a genius consultant who gets total amnesia the instant they walk out the door.</p>\n<h3>Why context stuffing breaks down</h3>\n<p>This cycle of \"context stuffing\" doesn't hold up in production. Relying only on a massive context window leads to high latency and the \"lost in the middle\" effect, where models ignore data buried in the center of a large prompt.</p>\n<p>To build agentic systems that actually remember, you must separate the short-lived context window from persistent long-term memory.</p>\n<p>A solid AI architecture mixes RAG, vector databases and careful context management. This guide breaks down the technical differences between context and memory, and how to build a hybrid that scales.</p>\n<h2>Understanding the context window</h2>\n<p>The context window is the model's immediate working memory. It is the total number of tokens (words or parts of words) the model can process at one moment.</p>\n<p>When you send a prompt to a frontier model like Claude or GPT, the window holds your current instruction, the previous conversation and any files you attached. To see how much of a window your own prompt uses, paste it into my <a href=\"https://ansezz.com/tools/context-window-checker/\">context window checker</a>.</p>\n<h3>The desk analogy</h3>\n<p>Think of the context window as a physical desk. You can only fit so many papers on it at once. Add a new stack of documents and you have to push the old ones off.</p>\n<p>Once a token falls out of this window, the model loses all awareness of it. It does not matter if the information was vital. It is gone.</p>\n<h3>Big windows are still temporary</h3>\n<p>Modern models offer very large context windows: current Claude and Gemini models both go up to one million tokens. That is impressive, but it is still temporary, and it is not a replacement for a database.</p>\n<p>Using a large window for everything is an expensive way to handle data that should be stored permanently. Bigger is not automatically better either: accuracy and recall drop as the window fills, a problem the field now calls \"context rot.\"</p>\n\nPaste your largest production prompt into the [context window checker](/tools/context-window-checker/) and mark which parts are the same on every request. Those repeated parts are your first candidates for memory instead of context.\n\n<h2>The illusion of long-term memory</h2>\n<p>Many developers mistake a long context window for long-term memory. It is a dangerous technical shortcut.</p>\n<p>If you are building for <a href=\"https://ansezz.com/blog/agentic-commerce-shopify/\">Shopify Plus</a>, for example, you might be tempted to dump your entire product catalog into the prompt. That creates three immediate problems:</p>\n<ol>\n<li><strong>Cost.</strong> You pay for those tokens every time the user asks a question.</li>\n<li><strong>Latency.</strong> The more tokens you send, the longer the model takes to reason and respond.</li>\n<li><strong>Accuracy.</strong> Large context windows are prone to noise. The model may hallucinate because it is overwhelmed by irrelevant data.</li>\n</ol>\n<h3>What real memory looks like</h3>\n<p>True long-term memory is persistent. It lives outside the model.</p>\n<p>It lets an AI agent remember a user's preference from three months ago without including that preference in every API call. This is where Retrieval-Augmented Generation (RAG) becomes the hero of your architecture.</p>\n<p>A bigger desk doesn't help if you never build the library.</p>\n<h2>RAG: the external brain for AI</h2>\n<p>Retrieval-Augmented Generation (RAG) is the bridge between a stateless LLM and a persistent knowledge base.</p>\n<p>Instead of stuffing everything into the context window, you store your data in a vector database. When a query comes in, you run a semantic search to find only the most relevant \"chunks\" of information.</p>\n<h3>Lean prompts, focused model</h3>\n<p>Those chunks are then injected into the context window. The prompt stays lean and the model stays focused. RAG turns the \"desk\" (context window) into a tool for reasoning over a \"library\" (vector database).</p>\n<h3>pgvector for Laravel teams</h3>\n<p>For teams in the Laravel ecosystem, RAG has become much easier with tools like <a href=\"https://github.com/pgvector/pgvector\">pgvector</a>. You can store embeddings directly in your PostgreSQL database, so your relational data and your AI logic live side by side.</p>\n<table>\n<thead>\n<tr>\n<th>Feature</th>\n<th>Context Window</th>\n<th>Long-Term Memory (RAG)</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Duration</strong></td>\n<td>Session-based (Temporary)</td>\n<td>Persistent (Permanent)</td>\n</tr>\n<tr>\n<td><strong>Capacity</strong></td>\n<td>Limited (up to ~1M tokens)</td>\n<td>Virtually Unlimited</td>\n</tr>\n<tr>\n<td><strong>Cost</strong></td>\n<td>High per-request cost</td>\n<td>Low per-request (fixed storage)</td>\n</tr>\n<tr>\n<td><strong>Update Speed</strong></td>\n<td>Instant for the session</td>\n<td>Requires indexing/embedding</td>\n</tr>\n</tbody>\n</table>\n<figure>\n<h2>The economics of tokens vs infrastructure</h2>\n<p>Choosing between a larger context window and a RAG pipeline is an economic decision.</p>\n<p>For a small internal tool with ten users, context stuffing is fine. The engineering hours needed to build a RAG pipeline would exceed the token savings.</p>\n<h3>When the math flips</h3>\n<p>For a SaaS application or a high-volume e-commerce store, the math shifts quickly. Sending 100,000 tokens per request at scale will destroy your margins. A well-optimized RAG system might send only 2,000 tokens per request.</p>\n<p>Run both numbers through my <a href=\"https://ansezz.com/tools/llm-cost-calculator/\">LLM cost calculator</a> to see the monthly gap.</p>\n\n  <div>About 100,000 tokens per request: the whole catalog and history, paid for every time, with slower answers and more noise.</div>\n  <div>About 2,000 tokens per request: only the top chunks from the vector store, with a small fixed storage cost.</div>\n\n<h3>Storage is cheaper than tokens</h3>\n<p>A vector database like Pinecone, or a self-hosted pgvector instance, often costs a fraction of the wasted tokens. The trade-offs between managed and self-hosted stores are worth weighing up front: see <a href=\"https://ansezz.com/blog/picking-the-right-rag-stack/\">picking the right RAG stack</a>.</p>\n<p>Either way, you avoid the common <a href=\"https://ansezz.com/blog/7-rag-mistakes-production/\">RAG mistakes in production</a> by focusing on retrieval quality rather than prompt size.</p>\n\nPut your current tokens per request and your daily request count into the [LLM cost calculator](/tools/llm-cost-calculator/), then repeat with 2,000 tokens per request. The difference is your monthly budget for building RAG.\n\n<h2>Implementing persistent memory with pgvector</h2>\n<p>If you run a Laravel application, you do not need a separate, complex vector database for basic memory. PostgreSQL with the pgvector extension is often the best choice for mid-sized applications. It lets you run similarity searches alongside your standard Eloquent queries.</p>\n<h3>Keywords vs meaning</h3>\n<p>Imagine a user searching for \"shoes for rainy weather.\" A traditional database looks for the keyword \"rainy.\" With embeddings and pgvector, the system understands the semantic link between \"rainy\" and \"waterproof.\"</p>\n<pre><code>// Example of a semantic search in Laravel using pgvector\n// Pseudocode: inject your embedder; there is no Laravel AI:: facade.\n$queryEmbedding = $embedder-&gt;embed(\"shoes for rainy weather\");\n\n$products = Product::query()\n    -&gt;selectRaw('name, description, embedding &lt;=&gt; ? as distance', [$queryEmbedding])\n    -&gt;orderBy('distance')\n    -&gt;limit(5)\n    -&gt;get();\n</code></pre>\n<p>By storing these embeddings, you create a persistent memory of your product catalog that is always ready for the LLM. You only pull the relevant products into the context window when they are needed.</p>\n<h2>Claude MCP and the future of context</h2>\n<p>The Model Context Protocol (MCP), created by Anthropic, changes how we handle context. It lets models connect directly to external data sources like Google Drive, Slack or your local filesystem.</p>\n<h3>Fetch on demand</h3>\n<p>Instead of you managing what goes into the context window, the model uses <a href=\"https://ansezz.com/blog/claude-mcp-dev-tools/\">Claude MCP servers</a> to fetch what it needs on demand.</p>\n<p>This blurs the line between context and memory. The model \"remembers\" where to find information and retrieves it when needed.</p>\n<h3>Reasoning over size</h3>\n<p>This is the foundation for agentic systems. An agent that can query its own database or read its own logs via MCP doesn't need a massive context window.</p>\n<p>It needs a high \"reasoning capacity\" to know which piece of memory to grab at the right time.</p>\n<figure>\n<h2>Hybrid strategies for agentic systems</h2>\n<p>Advanced AI systems do not choose one over the other. They use a tiered memory strategy:</p>\n<ol>\n<li><strong>Short-term context.</strong> The last 5 to 10 messages of the conversation for immediate flow.</li>\n<li><strong>Episodic memory.</strong> RAG-based retrieval of previous conversations with this specific user.</li>\n<li><strong>Semantic knowledge.</strong> RAG-based retrieval of the general knowledge base (manuals, docs, catalogs).</li>\n<li><strong>Tool-based memory.</strong> Tools (often via MCP) that fetch live data on demand, wrapped in <a href=\"https://ansezz.com/blog/circuit-breakers-vector-db/\">circuit breakers</a> so a slow tool doesn't stall the agent.</li>\n</ol>\n<p>This layered approach gives the model what it needs to solve a problem without the bloat of an oversized prompt.</p>\n\nSort everything in your current prompt into the four tiers above. Anything that isn't short-term context belongs in a store you retrieve from, not in every request.\n\n\n- **Stop context stuffing.** If your prompt is regularly over 50k tokens, you are likely wasting money and reducing accuracy.\n- **Use RAG for persistence.** Move static or long-term data into a vector database like pgvector or Pinecone.\n- **Optimize for latency.** Smaller prompts mean faster responses and a better user experience.\n- **Use MCP.** The Model Context Protocol gives your agents a standard way to \"reach out\" to their memory stores.\n- **Monitor token counts.** Track how many tokens actually help the final output and how many are noise.\n\n<p>As models get smarter, the \"size\" of the desk matters less than the \"organization\" of the library. If you're designing memory for a production agent, <a href=\"https://ansezz.com/services/\">here's how I help teams build RAG memory that stays fast and affordable</a>.</p>\n<p>Are you building a bigger desk, or a better library?</p>\n</figure></figure>",
      "date_published": "2026-06-04T00:00:00.000Z",
      "date_modified": "2026-06-04T00:00:00.000Z",
      "tags": [
        "ai",
        "rag",
        "pgvector",
        "llm",
        "mcp",
        "ai-engineering"
      ],
      "image": "https://ansezz.com/blog/context-window-vs-memory/hero-fd029f58.webp"
    },
    {
      "id": "https://ansezz.com/blog/data-engineer-vs-data-scientist/",
      "url": "https://ansezz.com/blog/data-engineer-vs-data-scientist/",
      "title": "Data engineer vs data scientist: roles, tools, and overlap",
      "summary": "Data engineer vs data scientist: who builds the pipelines, who builds the models, where the tools overlap, and which role your AI project needs first.",
      "content_html": "<p>Your AI is only as good as the data under it. The data engineer builds the factory; the data scientist refines the product.</p>\n\n- Data engineers work upstream: pipelines, warehouses, data models and reliable infrastructure that moves data from source to storage.\n- Data scientists work downstream: exploration, models, experiments and turning clean data into business answers.\n- AI and RAG projects need both, but the engineering foundation usually comes first, because you can't do science on broken pipes.\n\n<p>Your business is drowning in data, but your AI models are still hallucinating or returning irrelevant results.</p>\n<p>You have invested in the latest large language models and vector databases, yet the output stays inconsistent because the underlying data is a fragmented mess of raw logs and poorly formatted JSON.</p>\n<h3>The foundation problem</h3>\n<p>Without a solid foundation, even the most advanced neural network is a sophisticated guessing machine. The fix starts with understanding the distinct but complementary roles of the data engineer and the data scientist.</p>\n<h3>Same tools, different missions</h3>\n<p>These two roles often share an office and use the same languages, like Python and SQL, but their core missions are different.</p>\n<p>When you deploy <a href=\"https://ansezz.com/blog/agentic-workflows-vibe-coding/\">agentic systems</a> or RAG architectures, the split between infrastructure and inference decides whether a project scales or stays stuck in prototyping.</p>\n<h2>The architect vs. the detective</h2>\n<p>To see the difference, picture the construction of a smart city.</p>\n<h3>The engineer builds the utilities</h3>\n<p>The data engineer is the civil architect and utility provider. They design the power grids, the water filtration systems and the high-speed transit tunnels.</p>\n<p>Their goal is reliability, throughput and structural integrity. If the water stops flowing or the lights flicker, the city stops working.</p>\n<h3>The scientist plans the city</h3>\n<p>The data scientist is the urban planner and behavioral psychologist. They study traffic patterns, energy use and population growth to decide where to build the next park or how to improve the public transport schedule.</p>\n<p>They do not build the pipes. They use what flows through the pipes to make the city smarter and more efficient.</p>\n<h3>In a technical stack</h3>\n<p>The data engineer manages the flow of information from source to storage. The data scientist then pulls that information to train models, run experiments and generate business insights.</p>\n<p>One role keeps the data flowing. The other figures out what it means.</p>\n<h2>The role of the data engineer: building the pipeline</h2>\n<p>The data engineer designs, builds and maintains the systems that collect, store and move data. They work at the \"upstream\" end of the lifecycle, and their main focus is the \"three Vs\": Volume, Velocity and Variety.</p>\n<h3>Core responsibilities</h3>\n<ul>\n<li><strong>Pipeline development.</strong> Creating ETL (Extract, Transform, Load) or ELT processes that move data from sources like Shopify APIs, Google Cloud buckets or internal PostgreSQL databases into a central warehouse.</li>\n<li><strong>Infrastructure management.</strong> Setting up and managing data warehouses (Snowflake, BigQuery) and data lakes. This often involves <a href=\"https://ansezz.com/blog/coolify-docker-saas-hosting/\">Docker and Coolify</a> for self-hosted data tools, or managed services on AWS.</li>\n<li><strong>Data modeling.</strong> Designing the schema and architecture of the data so it is optimized for fast queries.</li>\n<li><strong>Reliability and scaling.</strong> Making sure the data infrastructure handles traffic spikes without crashing and that data stays consistent across all systems.</li>\n</ul>\n<h3>Thinking like a systems engineer</h3>\n<p>Data engineers spend a lot of time on system architecture and DevOps principles.</p>\n<p>They are the ones who add <a href=\"https://ansezz.com/blog/circuit-breakers-vector-db/\">circuit breakers for vector databases</a> and make sure your RAG pipeline doesn't fail because of a sudden surge in API requests.</p>\n\nPick one report your team relies on and trace where its data comes from. If you can't name every source and the job that moves it, that pipeline needs a data engineer.\n\n<h2>The role of the data scientist: extracting the value</h2>\n<p>Once the data is clean, formatted and accessible, the data scientist takes over. They work \"downstream,\" using curated datasets to solve specific business problems or build predictive features.</p>\n<h3>Core responsibilities</h3>\n<ul>\n<li><strong>Exploratory Data Analysis (EDA).</strong> Investigating datasets to find hidden patterns, outliers or correlations that can inform business decisions.</li>\n<li><strong>Machine learning development.</strong> Building and tuning models for classification, regression or recommendation systems, often with frameworks like PyTorch or TensorFlow.</li>\n<li><strong>A/B testing and experimentation.</strong> Designing and running tests to see how changes in a web application or Shopify store affect user behavior and conversion rates.</li>\n<li><strong>Data storytelling.</strong> Explaining complex mathematical findings to non-technical stakeholders through visualizations and reports.</li>\n</ul>\n<h3>The questions they answer</h3>\n<p>Data scientists ask questions like \"Why is our churn rate increasing?\" or \"What is the best price point for this new subscription service?\"</p>\n<p>They rely heavily on the data engineer for high-quality data. If the data is corrupted, the model will be biased or inaccurate.</p>\n<figure>\n<h2>Data engineer vs data scientist: a side-by-side view</h2>\n<p>The tools and skill sets overlap, but how they use them differs. Here is how the roles compare in a production environment.</p>\n<table>\n<thead>\n<tr>\n<th>Feature</th>\n<th>Data Engineer</th>\n<th>Data Scientist</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Primary Goal</strong></td>\n<td>Build and maintain data systems</td>\n<td>Analyze and model data for insights</td>\n</tr>\n<tr>\n<td><strong>Common Languages</strong></td>\n<td>SQL, Python, Java, Scala</td>\n<td>Python, R, SQL</td>\n</tr>\n<tr>\n<td><strong>Key Tools</strong></td>\n<td>Spark, Kafka, Airflow, dbt, Docker</td>\n<td>scikit-learn, PyTorch, Jupyter, Tableau</td>\n</tr>\n<tr>\n<td><strong>Main Output</strong></td>\n<td>Cleaned datasets, APIs, pipelines</td>\n<td>Predictive models, insights, reports</td>\n</tr>\n<tr>\n<td><strong>Mindset</strong></td>\n<td>Engineering and reliability</td>\n<td>Statistics and experimentation</td>\n</tr>\n<tr>\n<td><strong>Cloud Focus</strong></td>\n<td>Infrastructure (GCP, AWS, Terraform)</td>\n<td>Managed ML services (SageMaker, Vertex AI)</td>\n</tr>\n</tbody>\n</table>\n<h3>Same SQL, different job</h3>\n<p>A data scientist might use SQL to pull a specific cohort of users for analysis. A data engineer uses SQL to optimize a transformation job that processes millions of rows.</p>\n<h2>The synergy in modern AI stacks</h2>\n<p>With LLMs and generative AI, the lines between these roles are blurring, but you still need both specialists. A similar boundary is being redrawn between the <a href=\"https://ansezz.com/blog/ml-engineer-vs-ai-engineer/\">ML engineer and the AI engineer</a>. Consider a Retrieval-Augmented Generation (RAG) system.</p>\n<h3>The engineer's half of RAG</h3>\n<p>The data engineer builds the ingestion pipeline that scrapes documentation, cleans the text, generates embeddings and stores them in a vector database like pgvector.</p>\n<p>They make sure the <a href=\"https://ansezz.com/blog/api-gateway-ai-stack/\">API gateway</a> is secure and that the index stays fresh, whether through scheduled batch jobs or near-real-time streaming.</p>\n<h3>The scientist's half of RAG</h3>\n<p>The data scientist then evaluates how well the RAG system performs. They experiment with different embedding models, adjust the \"top-k\" retrieval parameters and fine-tune the prompts so the AI gives the most accurate and helpful responses.</p>\n\n  <div>A fast, reliable pipeline whose answers nobody checks, so bad results go unnoticed.</div>\n  <div>Good evaluation ideas and no fresh, clean data to run them on, because nobody owns ingestion.</div>\n\n<h3>Why you need both</h3>\n<p>Without the engineer, the scientist has no data to query. Without the scientist, nobody knows if the answers are any good.</p>\n<p>Both are essential for moving beyond basic chatbots and into <a href=\"https://ansezz.com/blog/agentic-commerce-shopify/\">agentic commerce</a> solutions.</p>\n<figure>\n\nFor your RAG system, name one owner for \"the index is fresh and complete\" and one owner for \"the answers are accurate.\" If both names are the same person, or blank, you have found your gap.\n\n<h2>Which role does your project need?</h2>\n<p>If you are a startup or an established business modernizing your digital presence, you might wonder which hire to make first.</p>\n<h3>You need a data engineer if</h3>\n<ul>\n<li>Your data is scattered across multiple SaaS platforms and spreadsheets.</li>\n<li>Your existing reports are slow or frequently crash.</li>\n<li>You want to build a real-time data streaming platform or a scalable AI infrastructure.</li>\n<li>You are migrating to the cloud (GCP or AWS) and need a custom pipeline.</li>\n</ul>\n<h3>You need a data scientist if</h3>\n<ul>\n<li>You have a large amount of clean data but do not know how to use it.</li>\n<li>You want to build recommendation engines or churn prediction models.</li>\n<li>You need to run complex experiments to optimize your marketing spend.</li>\n<li>You want to use AI to find specific insights that are not obvious from standard reporting.</li>\n</ul>\n<h3>Build the foundation first</h3>\n<p>Often, the best approach is to build the engineering foundation first. It is impossible to do science on a pile of broken pipes.</p>\n\n- **Data engineers** focus on the \"how\" of data movement and storage. They are software engineers specialized in distributed systems and infrastructure.\n- **Data scientists** focus on the \"what\" and \"why\" of the data. They are statisticians and analysts specialized in extracting meaning and building models.\n- **Collaboration** is key for AI success: a good RAG or agentic system needs solid engineering to feed solid science.\n- **Tooling** overlaps in Python and SQL, but engineers lean toward orchestration (Airflow, dbt) while scientists lean toward modeling (scikit-learn, PyTorch).\n- **Foundation first.** You can't do meaningful data science without a reliable data engineering infrastructure.\n\n<p>If your AI project is stuck because the data underneath it is a mess, <a href=\"https://ansezz.com/services/\">here's how I help teams build the data pipeline and RAG foundation first</a>.</p>\n<p>How are you currently handling the gap between your raw data infrastructure and your analytical insights?</p>\n</figure></figure>",
      "date_published": "2026-06-04T00:00:00.000Z",
      "date_modified": "2026-06-04T00:00:00.000Z",
      "tags": [
        "career",
        "machine-learning",
        "ai",
        "infrastructure",
        "databases",
        "career"
      ],
      "image": "https://ansezz.com/blog/data-engineer-vs-data-scientist/hero-1bf2211c.webp"
    },
    {
      "id": "https://ansezz.com/blog/cloud-engineer-vs-devops-engineer/",
      "url": "https://ansezz.com/blog/cloud-engineer-vs-devops-engineer/",
      "title": "Cloud engineer vs DevOps engineer in 2026",
      "summary": "Cloud engineer vs DevOps engineer: how their roles, skills, and tools differ, where they merge into platform engineering, and which one to hire first.",
      "content_html": "<p>A cloud engineer builds the ground your app stands on. A DevOps engineer builds the conveyor belt that ships code onto it. Hire the wrong one first and you pay for it.</p>\n\n- Cloud engineers design and run the infrastructure: networks, IAM, databases, storage and cloud cost (FinOps).\n- DevOps engineers automate delivery: CI/CD pipelines, deployments, observability and fast recovery.\n- The two roles overlap in infrastructure as code and often merge into platform engineering. Hire cloud first for a new foundation, DevOps first when releases are the bottleneck.\n\n<p>The cloud engineer vs DevOps engineer question trips up almost every team that outgrows a single server. The titles overlap, and job posts blur them together.</p>\n<p>Hiring the wrong specialty has a real cost: infrastructure that is over-engineered and expensive, or fast to ship but prone to outages. The two roles solve different problems. This guide breaks down where they diverge, where they overlap and which one to hire first.</p>\n<h2>The cloud engineer: designing the foundation</h2>\n<p>A cloud engineer acts as the architect of your digital workspace. Their main job is to design, build and manage the cloud infrastructure where your applications live.</p>\n<p>This role focuses on the \"where\" and the \"what\" of your stack. They spend their time in the large catalogs of providers like AWS, Google Cloud Platform (GCP) or Azure.</p>\n<h3>Building the environment</h3>\n<p>Cloud engineers specialize in the environment itself. That includes designing Virtual Private Clouds (VPCs), configuring subnets and managing identity and access management (IAM) policies.</p>\n<p>They make sure your database clusters are highly available and your storage is cost-effective.</p>\n<h3>Watching the bill</h3>\n<p>By 2026, cloud engineering has grown to include serious FinOps practices. Cloud engineers monitor resource consumption to prevent surprise bills from unoptimized cloud usage.</p>\n<h3>Leading migrations</h3>\n<p>When you need to move a legacy system to a modern cloud-native architecture, the cloud engineer leads the way. They translate business requirements into technical blueprints.</p>\n<p>For instance, they might design an <a href=\"https://ansezz.com/blog/api-gateway-ai-stack/\">API gateway architecture</a> to manage traffic for a distributed AI application. Their focus is the resilience, security and scalability of the infrastructure layer.</p>\n\nOpen last month's cloud bill and find the three most expensive line items. If nobody on the team can explain why each one costs what it does, you have a cloud engineering gap.\n\n<h2>The DevOps engineer: automating the lifecycle</h2>\n<p>While the cloud engineer builds the foundation, the DevOps engineer builds the machinery that moves code onto it.</p>\n<p>DevOps is a methodology focused on the software delivery lifecycle. A DevOps engineer is the bridge between development and operations teams, and their goal is to maximize the speed and reliability of releases through automation.</p>\n<h3>The pipeline is the core</h3>\n<p>The core of a DevOps engineer's toolkit is the <a href=\"https://ansezz.com/blog/ci-vs-cd/\">CI/CD pipeline</a> (continuous integration and continuous delivery). They use tools like GitHub Actions, GitLab CI or Jenkins to automate building, testing and deploying code.</p>\n<p>When a developer pushes a change, the DevOps engineer's systems scan it for vulnerabilities and deploy it to a staging environment without manual steps.</p>\n<h3>Reliability and observability</h3>\n<p>Reliability is another major pillar. DevOps engineers set up observability stacks with Prometheus and Grafana, and alerting that catches issues before they affect end users.</p>\n<p>In modern setups, they often use tools like Docker and <a href=\"https://ansezz.com/blog/coolify-docker-saas-hosting/\">Coolify for hosting</a> to simplify deployment workflows. They care deeply about the \"how\" of software delivery.</p>\n<figure>\n\nTime your last three deployments from merge to live, and count the manual steps in each. Long times or many manual steps point to a DevOps gap.\n\n<h2>Side-by-side comparison: skills and focus</h2>\n<p>To see which role fits your current needs, compare their daily priorities. There is a lot of overlap, but each role has a different center of gravity.</p>\n<p>Both rely heavily on infrastructure as code (IaC) with tools like Terraform or Pulumi, the same tooling I weigh up in <a href=\"https://ansezz.com/blog/terraform-vs-ansible/\">Terraform vs Ansible</a>.</p>\n<table>\n<thead>\n<tr>\n<th>Feature</th>\n<th>Cloud Engineer</th>\n<th>DevOps Engineer</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Primary Goal</strong></td>\n<td>Infrastructure design and management</td>\n<td>Automation and software delivery</td>\n</tr>\n<tr>\n<td><strong>Key Focus</strong></td>\n<td>Networking, Security, Storage, Cost</td>\n<td>CI/CD, Deployment, Monitoring, Culture</td>\n</tr>\n<tr>\n<td><strong>Cloud Expertise</strong></td>\n<td>Deep (multi-service architecture)</td>\n<td>Functional (deployment targets)</td>\n</tr>\n<tr>\n<td><strong>Coding Focus</strong></td>\n<td>Infrastructure scripts (Terraform, HCL)</td>\n<td>Glue code and automation (Bash, Python)</td>\n</tr>\n<tr>\n<td><strong>KPIs</strong></td>\n<td>Uptime, Cost efficiency, Security</td>\n<td>Deployment frequency, Mean Time to Recovery</td>\n</tr>\n<tr>\n<td><strong>Tools</strong></td>\n<td>AWS/GCP/Azure, VPC, IAM, RDS</td>\n<td>GitHub Actions, Docker, Kubernetes, Ansible</td>\n</tr>\n</tbody>\n</table>\n<p>Cloud engineers are often the ones who decide whether to use a managed database service or a self-hosted one. DevOps engineers make sure the migration to that database happens smoothly with zero downtime.</p>\n<p>One role decides where the system lives. The other decides how code gets there.</p>\n<h2>The overlap: platform engineering in 2026</h2>\n<p>The line between cloud and DevOps keeps getting blurrier. In high-growth startups and mature enterprises, the roles often merge into platform engineering.</p>\n<p>The platform team builds an \"Internal Developer Platform\" (IDP). It lets developers self-serve their own infrastructure needs within pre-defined guardrails.</p>\n<h3>Both skill sets in one team</h3>\n<p>Platform engineering combines the architecture knowledge of a cloud engineer with the automation mindset of a DevOps engineer. They use <a href=\"https://ansezz.com/blog/docker-vs-kubernetes/\">Kubernetes</a> (EKS, GKE or AKS) as the base layer.</p>\n<p>By hiding the complexity of the cloud, they help product teams move faster. If this sounds close to site reliability, it is. I unpack the boundaries in <a href=\"https://ansezz.com/blog/sre-vs-platform-engineer/\">SRE vs platform engineer</a>.</p>\n<h3>What it looks like for Laravel teams</h3>\n<p>For companies running Laravel applications, this might mean a standard Docker-based environment, so the code runs the same way on a local machine as it does in a production cluster on GCP.</p>\n<p>Browse the <a href=\"https://ansezz.com/blog/category/devops/\">DevOps category</a> to see how these workflows are structured in practice.</p>\n<figure>\n<h2>Choosing the right hire for your business</h2>\n<p>Which role to hire first depends on your current technical debt and business stage.</p>\n\n  <div>You are starting from scratch or moving off a single server. You need a secure, scalable foundation before anything is automated on top of it.</div>\n  <div>The cloud setup works, but developers burn hours on manual deployments and firefighting. You need a faster, safer release process.</div>\n\n<h3>Foundation first</h3>\n<p>If you are starting from scratch or moving away from a single-server setup, you need a cloud engineer. They will make your architecture secure and scalable from day one. Without this foundation, your automation will sit on top of a fragile environment.</p>\n<h3>Releases first</h3>\n<p>If you already have a working cloud setup but your developers are burning hours on manual deployments and firefighting, you need a DevOps engineer.</p>\n<p>They will untangle your release process and add the guardrails you need for rapid iteration. They focus on developer experience and velocity.</p>\n<h3>Hybrid needs</h3>\n<p>Large e-commerce businesses on Shopify Plus often have hybrid needs. They might need custom middleware hosted on cloud infrastructure, plus a strict CI/CD process for their custom Shopify apps.</p>\n<p>In these cases, a specialist who understands both the infrastructure and the automation pipeline is a strong first hire.</p>\n\nWrite one sentence for each role describing the problem it would solve on your team this quarter. Hire for the sentence that names a problem costing you money or outages today.\n\n\n- **Cloud engineers** specialize in infrastructure architecture, cloud-native services, security and cost optimization.\n- **DevOps engineers** focus on the software delivery pipeline, automation, CI/CD and system reliability.\n- **Tools overlap** heavily: both roles need Terraform, Docker and at least one major cloud provider.\n- **Platform engineering** combines both roles to give developers self-service infrastructure.\n- **Hire a cloud engineer** when you need to build or modernize the \"hardware\" of your digital business.\n- **Hire a DevOps engineer** when you need to speed up your release cycle and automate manual operational tasks.\n\n<p>If you're deciding between a cloud hire and a DevOps hire, or need both skill sets for a while, <a href=\"https://ansezz.com/services/\">here's how I help teams balance infrastructure and delivery</a>.</p>\n<p>How does your current team balance the need for architectural stability with the demand for rapid feature delivery?</p>\n</figure></figure>",
      "date_published": "2026-06-03T00:00:00.000Z",
      "date_modified": "2026-06-03T00:00:00.000Z",
      "tags": [
        "career",
        "devops",
        "infrastructure",
        "ci-cd",
        "cloud-platforms",
        "career"
      ],
      "image": "https://ansezz.com/blog/cloud-engineer-vs-devops-engineer/hero-396e1870.webp"
    },
    {
      "id": "https://ansezz.com/blog/cdn-vs-cache/",
      "url": "https://ansezz.com/blog/cdn-vs-cache/",
      "title": "CDN vs cache: why your high-traffic site needs both",
      "summary": "CDN and cache solve different problems: distance vs repeated work. How to layer edge delivery and server-side caching for high-traffic sites.",
      "content_html": "<p>A cache stops your server from doing the same work twice. A CDN stops your data from crossing the world twice. A high-traffic site needs both.</p>\n\n- Caching is a technique: store the result of expensive work (queries, rendered fragments, assets) so you don't repeat it.\n- A CDN is a global network of edge servers that applies caching close to the user, which cuts the distance and latency of each request.\n- Layer them (browser, CDN edge, server cache, database) and plan purging from day one, or users will see stale data.\n\n<p>High-traffic applications often collapse under the weight of redundant data requests.</p>\n<p>Every time a user in London requests a file from a server in New York, the latency hurts the user experience and drains your server resources.</p>\n<p>If you do not optimize how data is stored and delivered, your infrastructure costs will climb while your performance plateaus.</p>\n<h3>Two tools, two jobs</h3>\n<p>Many developers use \"CDN\" and \"cache\" interchangeably, but they serve different masters in the stack.</p>\n<p>The CDN-vs-cache decision is rarely either/or. Getting a global CDN and local caching to work together is what keeps a high-traffic site fast under load.</p>\n<h2>The fundamental speed gap</h2>\n<p>Latency is the silent killer of conversion rates. A standard request involves multiple round-trips between the client and the origin server.</p>\n<p>If the data has to cross the Atlantic, physical distance imposes a speed limit that no amount of code optimization can fix.</p>\n<p>This is a latency problem, not a bandwidth one. A <a href=\"https://ansezz.com/blog/bandwidth-vs-throughput/\">wider pipe won't fix the lag</a> when the bottleneck is round-trip distance.</p>\n<h3>Closer to the work, closer to the user</h3>\n<p>Caching keeps data closer to the execution point. A CDN keeps that cache closer to the user.</p>\n<p>When the two work together, they form a multi-layered shield that protects your origin server from unnecessary load.</p>\n<p>Code can't beat the speed of light. Only a closer server can.</p>\n<h2>Understanding the cache hierarchy</h2>\n<p>Caching is the temporary storage of data for faster retrieval. It is not limited to one location. In a modern stack, caching happens at several layers so the most expensive operations are never repeated.</p>\n<p>The most common layers:</p>\n<ul>\n<li><strong>Browser cache.</strong> Assets like CSS, JS and images are stored on the user's device. Repeat visits feel instant.</li>\n<li><strong>Server cache.</strong> Tools like Redis or Memcached keep database query results and computed objects in memory, so the app doesn't hit the database for every request. The same in-memory layer powers more advanced patterns too, like <a href=\"https://ansezz.com/blog/redis-semantic-caching-rag/\">semantic caching in RAG pipelines</a>.</li>\n<li><strong>Application cache.</strong> Frameworks like <a href=\"https://ansezz.com/blog/category/laravel/\">Laravel</a> have built-in ways to cache entire HTML fragments or API responses.</li>\n</ul>\n<h3>The trade</h3>\n<p>The goal of caching is to reduce the workload on your primary infrastructure. By storing the result of a complex calculation or a heavy query, you trade a small amount of memory for a large gain in response time.</p>\n\nTurn on slow query logging for a day and list the five queries that run most often with the same parameters. Those are your first server-cache candidates.\n\n<h2>The global reach of CDNs</h2>\n<p>A CDN is a specialized cache that lives at the \"edge\" of the network.</p>\n<p>A standard cache might live on your main server. A CDN is a globally distributed network of servers that hosts your content in dozens of locations at once.</p>\n<h3>How a CDN request works</h3>\n<p>When a user visits your site, the CDN intercepts the request, acting as a <a href=\"https://ansezz.com/blog/forward-proxy-vs-reverse-proxy/\">reverse proxy</a> in front of your origin.</p>\n<p>If the requested asset is already stored at the edge server closest to the user, it is served immediately, without the trip back to your origin. This matters most for static assets like images, video files and large JavaScript bundles.</p>\n<h3>Shopify's built-in CDN</h3>\n<p>For businesses on <a href=\"https://ansezz.com/blog/category/shopify/\">Shopify</a>, the platform handles much of this CDN logic automatically. Its CDN (backed by Cloudflare) ships on every plan, not only Plus.</p>\n<p>Even so, understanding how to manage cache headers and purge cycles remains a key skill for custom development.</p>\n<figure>\n<h2>Technical comparison: CDN vs cache</h2>\n<p>To choose the right tool, you need to know where each one excels.</p>\n<table>\n<thead>\n<tr>\n<th>Feature</th>\n<th>Local/Server Cache</th>\n<th>Content Delivery Network (CDN)</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Primary Goal</strong></td>\n<td>Reduce CPU and database load.</td>\n<td>Reduce network latency and distance.</td>\n</tr>\n<tr>\n<td><strong>Location</strong></td>\n<td>On or near the origin server.</td>\n<td>At the network edge (Points of Presence).</td>\n</tr>\n<tr>\n<td><strong>Scope</strong></td>\n<td>Often specific to a single server or user.</td>\n<td>Shared across many users globally.</td>\n</tr>\n<tr>\n<td><strong>Ownership</strong></td>\n<td>Part of your application infrastructure.</td>\n<td>Typically a third-party service provider.</td>\n</tr>\n<tr>\n<td><strong>Best For</strong></td>\n<td>DB queries, session data, fragments.</td>\n<td>Images, JS, CSS, static HTML, video.</td>\n</tr>\n</tbody>\n</table>\n<p>Caching is about how data is stored. A CDN is about where that data is distributed.</p>\n<h2>Implementation in Laravel and Shopify</h2>\n<p>How you implement this depends on your stack.</p>\n<h3>Laravel</h3>\n<p>In a <a href=\"https://ansezz.com/blog/category/laravel/\">Laravel environment</a>, you manage caching through a unified API that supports multiple drivers.</p>\n<pre><code>// Example of caching a database query in Laravel\n$users = Cache::remember('active_users', 3600, function () {\n    return DB::table('users')-&gt;where('active', true)-&gt;get();\n});\n</code></pre>\n<p>This queries the database only once per hour for this data.</p>\n<p>To go further, configure your web server to set <code>Cache-Control</code> headers that a CDN like Cloudflare or Akamai can interpret. My <a href=\"https://ansezz.com/tools/http-headers/\">HTTP headers cheat sheet</a> lists the caching headers with real example values.</p>\n\nRun `curl -I` against one image, one JS bundle and one HTML page on your site. Check the `Cache-Control` value and the CDN's cache status header on each, and fix any static asset that says `no-cache`.\n\n<h3>Shopify</h3>\n<p>In <a href=\"https://ansezz.com/blog/category/shopify/\">Shopify development</a>, you rely heavily on the platform's native CDN. Shopify uses a large edge network to serve product images and storefront assets.</p>\n<p>When you build custom apps or headless storefronts, though, you must manage how your API responses are cached yourself to avoid performance bottlenecks.</p>\n<h2>Using both for maximum scale</h2>\n<p>The most resilient architectures combine a \"cache-aside\" pattern with an edge delivery strategy. This creates a multi-step defense for your origin server:</p>\n<ol>\n<li><strong>The edge shield.</strong> The CDN handles most incoming traffic for static files and common public pages.</li>\n<li><strong>The server shield.</strong> For requests that reach the origin, a server-side cache (like Redis) returns data without hitting the database.</li>\n<li><strong>The database shield.</strong> Proper indexing and internal database caching provide a final layer of optimization.</li>\n</ol>\n<p>This layered approach is essential for <a href=\"https://ansezz.com/blog/category/devops/\">modern DevOps</a>. Even during a massive traffic spike, like a Black Friday sale or a viral product launch, your core application stays responsive. For the application tier itself, see how <a href=\"https://ansezz.com/blog/laravel-octane-high-traffic/\">Laravel Octane handles high traffic</a>.</p>\n\n  <div>Every visitor's request travels to the origin, runs the same queries and renders the same page. A traffic spike hits the database directly.</div>\n  <div>The CDN answers most requests at the edge, Redis answers most of the rest, and only cache misses reach an indexed database.</div>\n\n<figure>\n<h2>Common pitfalls to avoid</h2>\n<p>Running two caching systems brings the challenge of cache invalidation.</p>\n<p>If you update a product price in your database but the CDN still serves a cached version of the page, you will face customer service issues.</p>\n<h3>Build a purge strategy</h3>\n<p>When data changes on your origin, you need a way to clear both your internal cache and the matching edge cache on your CDN.</p>\n<p>Many developers use webhooks or event listeners to automate this. Users then see accurate data without you giving up speed.</p>\n\nChange one product price in staging and time how long it takes to show up on the live page through the CDN. If the answer is \"until the TTL expires,\" wire a purge call into the save event.\n\n\n- **Caching** is the technique of storing data temporarily to avoid repeating expensive operations.\n- **A CDN** is a network of servers that use caching to deliver content from a location physically close to the user.\n- **Distance matters.** A CDN reduces the travel time of data, while a local cache reduces the processing time.\n- **Use both.** Cache database results on the server and serve static assets from a CDN.\n- **Invalidation is key.** Make sure you can purge outdated data from all layers together to keep data accurate.\n\n<p>If your site slows down every time traffic spikes, <a href=\"https://ansezz.com/services/\">here's how I help teams layer CDN and server caching for high traffic</a>.</p>\n<p>How do you handle cache invalidation across multiple edge locations when your application data changes in real time?</p>\n</figure></figure>",
      "date_published": "2026-06-02T00:00:00.000Z",
      "date_modified": "2026-06-02T00:00:00.000Z",
      "tags": [
        "devops",
        "networking",
        "redis",
        "performance",
        "laravel",
        "shopify",
        "infrastructure"
      ],
      "image": "https://ansezz.com/blog/cdn-vs-cache/hero-684cfc27.webp"
    },
    {
      "id": "https://ansezz.com/blog/ci-vs-cd/",
      "url": "https://ansezz.com/blog/ci-vs-cd/",
      "title": "CI vs CD: automating quality and delivery",
      "summary": "Continuous Integration vs Continuous Delivery explained. How to automate the path from commit to production for Laravel backends and Shopify storefronts.",
      "content_html": "<p>CI proves your code works. CD gets that working code to users without a human copying files at midnight.</p>\n\n- Continuous Integration builds and tests every push, so integration bugs show up in minutes instead of at release time.\n- Continuous Delivery keeps every passing build ready to ship behind a manual button. Continuous Deployment removes the button.\n- For Laravel, that means a clean-room test pipeline. For Shopify, it means theme checks and deploying to an unpublished theme before you swap it live.\n\n<p>Shipping a broken build to production is an expensive way to learn your local environment isn't a mirror of reality.</p>\n<p>For developers on Laravel backends or Shopify storefronts, the gap between \"it works on my machine\" and a stable release is often filled with manual steps that invite human error.</p>\n<p>Manual FTP uploads, SSHing into servers to run migrations, or publishing Shopify themes by hand are risks modern engineering cannot afford.</p>\n<h3>Two pillars</h3>\n<p>Continuous Integration (CI) and Continuous Delivery (CD) remove this uncertainty. By automating the path from a commit to a live environment, teams ship faster while making their applications more stable.</p>\n<p>This guide breaks down the core differences between CI and CD and how to apply them to modern stacks.</p>\n<h2>Defining Continuous Integration (CI)</h2>\n<p>Continuous Integration is the practice of merging every developer's working copy into a shared mainline several times a day.</p>\n<p>In a Laravel or Vue.js environment, every push to a branch makes an automated system build the application and run a suite of tests.</p>\n<h3>Catching integration hell early</h3>\n<p>The goal of CI is to catch \"integration hell\" before it happens. Instead of finding out at a weekly release that two developers changed the same core service, the pipeline flags the conflict within minutes.</p>\n<p>A standard CI pipeline for a Laravel application includes:</p>\n<ul>\n<li><strong>Dependency management:</strong> installing Composer and NPM packages so the environment is fresh.</li>\n<li><strong>Static analysis:</strong> running tools like PHPStan or Laravel Pint to enforce code quality and style.</li>\n<li><strong>Automated testing:</strong> running unit and feature tests with PHPUnit or Pest.</li>\n<li><strong>Security audits:</strong> scanning third-party dependencies for known vulnerabilities.</li>\n</ul>\n<p>By the time a pull request is ready for review, CI has already given a \"green light\" that the code is syntactically correct and doesn't break any behavior your tests cover.</p>\n\nOpen your last five merged pull requests and check which of the four steps above actually ran on each. Any missing step is the next job to add to your pipeline.\n\n<h2>Defining Continuous Delivery and Deployment (CD)</h2>\n<p>CI focuses on the quality of the code. CD focuses on the delivery of that code. Two flavors of CD often get confused.</p>\n<h3>Continuous Delivery</h3>\n<p>Every build that passes the CI pipeline is ready to deploy to production, but the actual release needs a manual trigger.</p>\n<p>This is common in regulated industries, or for high-stakes storefronts where a marketing manager wants to time a release.</p>\n<h3>Continuous Deployment</h3>\n<p>This removes the manual trigger. Every change that passes the pipeline goes to production automatically.</p>\n<p>It demands deep confidence in your test suite and the <a href=\"https://ansezz.com/blog/logging-vs-monitoring/\">observability to catch regressions fast</a>. Without it, a bad commit reaches users before anyone notices.</p>\n\n  <div>Every green build is packaged and ready. A person presses the release button when the timing is right.</div>\n  <div>Every green build goes live on its own. Tests and monitoring are the only gate.</div>\n\n<h3>Self-hosted CD with Coolify</h3>\n<p>For developers using <a href=\"https://ansezz.com/blog/coolify-self-hosted-saas/\">Coolify for self-hosted SaaS</a>, CD is the engine behind rapid iteration. Once CI passes, Coolify can pull the latest Docker image and update the running containers with zero downtime.</p>\n<figure>\n<h2>CI vs CD: the core differences</h2>\n<p>The main difference is scope and goal. CI is about the developer's experience and code integrity. CD is about the user's experience and the release process.</p>\n<table>\n<thead>\n<tr>\n<th>Feature</th>\n<th>Continuous Integration (CI)</th>\n<th>Continuous Delivery (CD)</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Primary goal</strong></td>\n<td>Detect bugs and integration issues early.</td>\n<td>Ensure code is always in a releasable state.</td>\n</tr>\n<tr>\n<td><strong>Trigger</strong></td>\n<td>Triggered by every code commit or push.</td>\n<td>Triggered after CI passes successfully.</td>\n</tr>\n<tr>\n<td><strong>Key output</strong></td>\n<td>A validated code base and test reports.</td>\n<td>A deployable artifact (Docker image, ZIP, etc.).</td>\n</tr>\n<tr>\n<td><strong>Manual step</strong></td>\n<td>Fully automated process.</td>\n<td>Manual approval (Delivery) or fully automated (Deployment).</td>\n</tr>\n</tbody>\n</table>\n<p>In 2026, the lines are blurring as tools become more integrated. Most teams treat CI and CD as one pipeline where code flows from a developer's IDE into staging or production.</p>\n<p>CI asks \"is this code good?\" CD asks \"can users have it now?\"</p>\n<h2>Implementing CI for Laravel applications</h2>\n<p>Laravel gives you a solid base for CI because of its built-in testing tools. When you set up a pipeline, aim for a \"clean room\" environment: pin the same PHP and MySQL versions you run in production.</p>\n<p>A typical GitHub Actions workflow for Laravel might look like this:</p>\n<pre><code>name: Laravel CI\n\non: [push, pull_request]\n\njobs:\n  laravel-tests:\n    runs-on: ubuntu-latest\n    services:\n      mysql:\n        image: mysql:8.4\n        env:\n          MYSQL_DATABASE: testing\n          MYSQL_ALLOW_EMPTY_PASSWORD: \"yes\"\n        ports:\n          - 3306:3306\n        options: &gt;-\n          --health-cmd=\"mysqladmin ping\"\n          --health-interval=10s --health-timeout=5s --health-retries=5\n\n    steps:\n      - uses: actions/checkout@v7\n      - name: Setup PHP\n        uses: shivammathur/setup-php@v2\n        with:\n          php-version: \"8.5\"\n      - name: Install Dependencies\n        run: composer install --prefer-dist --no-interaction\n      - name: Execute Tests\n        run: vendor/bin/phpunit\n</code></pre>\n<p>With branch protection requiring this check, no code merges into main unless it passes every test case. That safety net lets the team move fast without fear of breaking core application logic.</p>\n\nTurn on branch protection for `main` and mark your CI job as a required status check. Until you do, a red build is only a suggestion.\n\n<h2>CD best practices for Shopify storefronts</h2>\n<p>Shopify development has traditionally meant editing themes in the browser. With the rise of <a href=\"https://ansezz.com/blog/agentic-commerce-shopify/\">agentic commerce on Shopify</a>, the need for real CI/CD is much higher.</p>\n<p>For Shopify, CD is about managing \"theme versions\" rather than server binaries. With the Shopify CLI, you can automate theme deployments to different environments:</p>\n<ol>\n<li><strong>Staging stores.</strong> Always deploy to a development or staging store first.</li>\n<li><strong>Theme checks.</strong> Run <code>shopify theme check</code> in your CI pipeline to catch Liquid errors before they go live.</li>\n<li><strong>Atomic deploys.</strong> Push your code to a new, unpublished theme ID. Once verified, use the CLI to publish it as the live theme.</li>\n</ol>\n<h3>Why the atomic swap matters</h3>\n<p>The cutover is instant, so customers never see a half-deployed storefront. It is a blue-green style of deployment for e-commerce.</p>\n<p>A broken layout or a missing \"Add to Cart\" button caused by a Liquid syntax error never reaches the live store.</p>\n\nAdd `shopify theme check` as a CI step on your theme repo, then do your next release by pushing to an unpublished theme and publishing it from the CLI instead of the theme editor.\n\n<figure>\n<h2>Going beyond unit tests</h2>\n<p>CI/CD pays off most when you move past simple tests and add more advanced checks.</p>\n<p>Modern pipelines in 2026 often include visual regression testing. The system takes screenshots of the UI and compares them to the previous version to catch unintended layout shifts.</p>\n<h3>Pipelines for AI-backed apps</h3>\n<p>For teams building <a href=\"https://ansezz.com/blog/api-gateway-ai-stack/\">API gateways for the AI stack</a>, CI/CD is non-negotiable.</p>\n<p>When your application relies on LLMs or external APIs, the pipeline must include integration tests that confirm those connections still work and behave as expected. If agents write part of your code, see the <a href=\"https://ansezz.com/blog/testing-ai-generated-code/\">gate I run on AI-authored PRs</a>.</p>\n\n- **CI is for quality.** Run tests, lint code and check for security vulnerabilities on every push.\n- **CD is for delivery.** Automate packaging and deployment to staging or production.\n- **Speed is a feature.** A CI pipeline that takes 30 minutes is a pipeline developers will try to bypass.\n- **Immutable artifacts.** Build once (e.g., a Docker image) and promote that same artifact through staging and production.\n- **Zero downtime.** Use blue-green deployments or symlink swaps so users aren't interrupted during a release.\n- **Shift left.** Move security and quality checks as early as possible to cut the cost of fixing bugs.\n\n<p>If your deployment process still involves a checklist a human has to follow, you aren't doing CD. Automation is the only way to scale a software business without scaling production outages.</p>\n<p>If you want your Laravel or Shopify releases to go from commit to production without the manual checklist, <a href=\"https://ansezz.com/services/\">here's how I help teams automate delivery</a>.</p>\n<p>How much time does your team spend manually verifying releases before they go live?</p>\n</figure></figure>",
      "date_published": "2026-06-02T00:00:00.000Z",
      "date_modified": "2026-06-02T00:00:00.000Z",
      "tags": [
        "devops",
        "devops",
        "ci-cd",
        "laravel",
        "shopify"
      ],
      "image": "https://ansezz.com/blog/ci-vs-cd/hero-e4f165f0.webp"
    },
    {
      "id": "https://ansezz.com/blog/api-vs-mcp/",
      "url": "https://ansezz.com/blog/api-vs-mcp/",
      "title": "API vs MCP: connecting AI systems",
      "summary": "The core differences between traditional APIs and the Model Context Protocol (MCP), and when to use each to build scalable, agentic AI systems.",
      "content_html": "<p>APIs connect software to software. MCP connects AI agents to software. Mix them up and you spend weeks writing glue code that breaks on the next schema change.</p>\n\n- Traditional APIs are rigid contracts built for developers who read docs. MCP is a standard, self-describing layer built for models that discover tools at runtime.\n- MCP turns the M x N integration problem (every AI client times every tool) into M + N.\n- MCP does not replace your APIs. It usually wraps them, so your business logic and security stay in your core services.\n\n<p>Your AI agents are trapped in a silo of brittle, custom-coded API integrations that break the moment you update a schema.</p>\n<p>You spend weeks writing boilerplate glue code just to give a Large Language Model (LLM) access to a single database or a CRM.</p>\n<p>This manual mapping of endpoints to prompts is the hidden tax of modern AI engineering. Every new tool needs a fresh round of documentation reading and prompt tuning.</p>\n<h3>Why APIs alone struggle with agents</h3>\n<p>APIs have powered the internet for decades, but they were never designed for the non-deterministic, reasoning-heavy nature of AI agents.</p>\n<p>We are moving from hard-coded connections to dynamic capability discovery. This is where the Model Context Protocol (MCP) comes in.</p>\n<h2>The API era: the power grid of the web</h2>\n<p>Traditional APIs (Application Programming Interfaces) are the foundation of modern software. Whether it is a RESTful endpoint, a GraphQL query or a gRPC stream, the core idea is the same.</p>\n<p>It is a defined contract for software-to-software communication. You know exactly what input to send, and you get a predictable output back.</p>\n<h3>Rigid by design</h3>\n<p>In AI development, APIs are like the power grid. They provide the raw energy and data your application needs.</p>\n<p>If you are building a <a href=\"https://ansezz.com/blog/agentic-commerce-shopify/\">Shopify application</a>, you use the Shopify Admin API to fetch orders or update inventory. The logic is rigid, the schemas are fixed, and the developer must know exactly which endpoint to call and when.</p>\n<h3>The translation tax</h3>\n<p>The problem starts when you want an LLM to use these tools. You have to write \"tool definitions\" or \"function schemas\" that describe the API to the model.</p>\n<p>You are translating the API documentation into a format the model can understand. This works for one or two tools. It does not scale to an entire ecosystem of internal data sources and external services.</p>\n<h2>What is Model Context Protocol?</h2>\n<p>The Model Context Protocol (MCP) is an open standard introduced by Anthropic to solve the \"M x N\" integration problem. Anthropic donated it to the Agentic AI Foundation, a Linux Foundation fund, in December 2025.</p>\n<p>In a traditional setup, if you have M AI clients (like Claude, ChatGPT and an internal agent) and N tools (like GitHub, Slack and your internal DB), you build M x N individual integrations.</p>\n<h3>From M x N to M + N</h3>\n<p>MCP changes this to an M + N problem. With a standard \"universal plug,\" any MCP-compatible AI client can connect to any MCP-compliant server. Think of it as the USB-C of the AI world.</p>\n<p>Instead of writing custom code to explain how to search your database, you host an MCP server that describes its own capabilities. When the agent connects, it \"discovers\" what the server can do without you hard-coding the instructions.</p>\n<h3>What runs under the hood</h3>\n<p>MCP uses JSON-RPC 2.0. A server can offer tools, resources (like files or data) and prompt templates to the model.</p>\n<p>Since the <code>2026-07-28</code> spec, the protocol core is stateless request and response. The connection handshake and session IDs are gone, and each request carries its own protocol version and client capabilities.</p>\n<p>If your app needs state across calls, the server hands the model an explicit handle to pass back. I cover what that means in practice in <a href=\"https://ansezz.com/blog/stateless-mcp-servers-laravel/\">stateless MCP servers on Laravel</a>.</p>\n\nCount the integrations you maintain today: AI clients times tools. If the number is above five or six, sketch which tools would become one MCP server each.\n\n<figure>\n<h2>Structural differences: discovery and consumers</h2>\n<p>The core difference between API and MCP lies in who consumes the interface and how they find what they need.</p>\n<table>\n<thead>\n<tr>\n<th>Feature</th>\n<th>Traditional API</th>\n<th>Model Context Protocol (MCP)</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Primary Consumer</strong></td>\n<td>Human Developers</td>\n<td>AI Agents &amp; LLMs</td>\n</tr>\n<tr>\n<td><strong>Discovery</strong></td>\n<td>Manual (Reading Docs)</td>\n<td>Automatic (Self-describing)</td>\n</tr>\n<tr>\n<td><strong>Interface</strong></td>\n<td>Heterogeneous (REST/GraphQL)</td>\n<td>Standardized (JSON-RPC)</td>\n</tr>\n<tr>\n<td><strong>State</strong></td>\n<td>Mostly Stateless</td>\n<td>Stateless core since <code>2026-07-28</code>; explicit handles for app state</td>\n</tr>\n<tr>\n<td><strong>Flexibility</strong></td>\n<td>Rigid / Pre-scripted</td>\n<td>Dynamic / Reasoning-based</td>\n</tr>\n</tbody>\n</table>\n<h3>Docs for humans, descriptions for models</h3>\n<p>APIs are built for humans to read and implement. You look at a Swagger UI, understand the authentication and write a fetch request.</p>\n<p>MCP is built for models to navigate. An MCP server lists its tools and resources along with natural language descriptions. The model reasons about which tool to call based on the user's intent.</p>\n<p>If you are <a href=\"https://ansezz.com/blog/ai-vs-traditional-development/\">comparing AI vs traditional development</a>, this is a prime example of the shift. Traditional development builds the pipes. AI engineering with MCP builds the connectors that let the model choose its own pipes.</p>\n<p>Developers read API docs. Models read MCP descriptions.</p>\n<h2>Why AI agents need MCP for scale</h2>\n<p>Agentic systems need a level of autonomy that traditional APIs struggle to provide efficiently.</p>\n<p>An agent asked to \"research a company and draft a proposal\" might need a search tool, a CRM, a file system and a PDF generator.</p>\n<h3>Less context, fewer wrong choices</h3>\n<p>With standard APIs, you feed the descriptions of all those endpoints into the model's context window. This eats tokens and raises the risk of the model getting confused.</p>\n<p>With MCP, the client asks the server what it offers (and can cache that list), and the agent calls tools on demand. You can surface only the tools relevant to the task instead of pre-loading every schema.</p>\n<p>The result is less wasted context and fewer irrelevant choices for the model to wade through.</p>\n<h3>Resources that can notify you</h3>\n<p>MCP also defines a \"resource\" primitive. An API endpoint returns data on request. An MCP resource can be subscribed to, so the server sends a notification when the underlying file or record changes.</p>\n<h3>Lightweight RAG, not a vector store</h3>\n<p>For lightweight retrieval-augmented generation (RAG) cases, an MCP server can expose your local files or database to the model in a standard way. That spares you a custom ingestion layer for every experiment.</p>\n<p>It does not replace a vector store for large-scale semantic search. Get that wrong and you hit the usual <a href=\"https://ansezz.com/blog/7-rag-mistakes-production/\">production RAG mistakes</a>.</p>\n<h2>Implementation: how they work together</h2>\n<p>MCP does not replace APIs. In fact, MCP usually wraps existing APIs. Your business logic, security constraints and data persistence still live in your core services.</p>\n<h3>A Laravel example</h3>\n<p>Say you have a Laravel backend with REST endpoints for managing customer orders.</p>\n<p>To make them accessible to an AI agent, you build a small MCP server (perhaps in Node.js or Python) that sits between the agent and your Laravel API.</p>\n<h3>The translation layer</h3>\n<p>The MCP server describes the <code>get_order_history</code> tool in a way an LLM understands.</p>\n<p>When the LLM calls that tool through MCP, the server makes the actual REST call to your Laravel backend, receives the JSON and passes the relevant context back to the model.</p>\n<p>This separation lets your core engineering team focus on solid, well-tested APIs while your AI team builds the MCP wrappers that enable agentic behavior.</p>\n\n  <div>Each AI client gets hand-written function schemas for each endpoint. Change the API and you update every client's prompt.</div>\n  <div>One MCP server describes the tools and calls your REST API. Every MCP client discovers the change on its next list call.</div>\n\n\nPick one read-only endpoint, like order history, and wrap it as a single MCP tool with a clear one-sentence description. Test it from an MCP client before you expose anything that writes data.\n\n<figure>\n<h2>Choosing the right tool for the job</h2>\n<p>When should you stick to direct API calls, and when should you implement MCP? It depends on the complexity of your integrations and the role of the LLM.</p>\n<h3>Use direct APIs when</h3>\n<ol>\n<li>You have a simple request-response flow with no AI reasoning involved.</li>\n<li>You need absolute minimum latency for high-throughput systems.</li>\n<li>You are performing deterministic tasks like processing payments or bulk data migrations.</li>\n<li>You only have one or two integrations that rarely change.</li>\n</ol>\n<h3>Use MCP when</h3>\n<ol>\n<li>You are building complex agents that need three or more tools.</li>\n<li>Your tools and data sources change frequently.</li>\n<li>You want your tools reusable across different AI clients (e.g., <a href=\"https://ansezz.com/blog/claude-mcp-dev-tools/\">Claude Desktop and custom IDE tools</a>).</li>\n<li>You need to expose local data or specialized resources to an LLM without building a custom backend for every experiment.</li>\n</ol>\n<h3>Where MCP stops</h3>\n<p>One boundary worth drawing early: MCP covers the agent-to-tool axis only. The moment two agents need to delegate work to each other, you are in a different protocol's territory.</p>\n<p>See <a href=\"https://ansezz.com/blog/mcp-vs-a2a-vs-acp/\">MCP vs A2A vs ACP</a> for how the two layers stack.</p>\n\n- APIs are the \"what\" (the data and logic), while MCP is the \"how\" (how the AI interacts with that data).\n- MCP removes the M x N integration problem with a universal protocol for AI tool discovery.\n- Traditional APIs suit deterministic software-to-software paths; MCP suits non-deterministic AI-to-tool reasoning.\n- Implementing MCP often means wrapping existing REST or GraphQL APIs to make them self-describing for LLMs.\n- MCP cuts token waste and prompt complexity by standardizing how context is shared with the model.\n- For modern AI engineering, MCP is becoming the standard layer for building agentic workflows that scale.\n\n<p>If you're wrapping your existing APIs in MCP so agents can use them safely, <a href=\"https://ansezz.com/services/\">here's how I help teams ship it</a>.</p>\n<p>What is the biggest friction point you face when giving your AI agents access to your internal production databases?</p>\n</figure></figure>",
      "date_published": "2026-06-01T00:00:00.000Z",
      "date_modified": "2026-06-01T00:00:00.000Z",
      "tags": [
        "architecture",
        "mcp",
        "api-design",
        "ai",
        "llm",
        "agentic-ai",
        "architecture"
      ],
      "image": "https://ansezz.com/blog/api-vs-mcp/hero-67343ce8.webp"
    },
    {
      "id": "https://ansezz.com/blog/coolify-self-hosted-saas/",
      "url": "https://ansezz.com/blog/coolify-self-hosted-saas/",
      "title": "Ditch expensive cloud providers for self-hosted SaaS",
      "summary": "The cloud tax kills SaaS margins early. How self-hosted Coolify on cheap ARM instances at Hetzner or Oracle slashes a $500 AWS bill toward zero.",
      "content_html": "<p>Most SaaS founders overpay their cloud provider without noticing. The \"cloud tax\" eats your margins before you even find product-market fit.</p>\n\n- Managed cloud starts cheap with credits, then bills you for idle databases, NAT gateways and complexity you don't need yet.\n- Coolify gives you a self-hosted, git-push PaaS on your own server, with automatic SSL, one-click databases and PR previews.\n- ARM servers make it cheaper still: a small app can run on a Hetzner ARM box for about $13.50 a month, or on Oracle's Always Free tier.\n\n<p>We have been conditioned to believe that unless our small CRUD app runs on a multi-region, auto-scaling AWS EKS cluster, we are doing it wrong.</p>\n<p>That is a lie designed to keep you paying for complexity you do not need.</p>\n<h2>The architecture of a trap</h2>\n<p>It starts innocently enough. You sign up for AWS or GCP because they give you $1,000 in credits.</p>\n<p>You spin up an RDS instance for your database, a few S3 buckets for storage, and maybe a managed Kubernetes service because it feels \"professional.\"</p>\n<p>Then the credits run out.</p>\n<h3>The bill after the credits</h3>\n<p>Suddenly you are paying $200 a month for a database that is 99% idle. You are paying for NAT gateways, provisioned IOPS and \"management fees\" for services that could easily run on a cheap VPS.</p>\n<p>You are stuck in a web of proprietary APIs and IAM roles that need a full-time DevOps engineer just to update an environment variable.</p>\n<h3>A golden cage</h3>\n<p>Managed services feel like a superpower at the start, but they become a golden cage as you scale. The complexity overhead alone can slow your development velocity to a crawl.</p>\n<p>When I look at how <a href=\"https://ansezz.com/blog/ai-vs-traditional-development/\">AI is changing traditional development</a>, it is clear we need to move faster, not get bogged down in infrastructure molasses.</p>\n\nSort your cloud bill by service and flag anything you pay for that runs under 10% utilization most of the month. That list is what a single self-hosted server can absorb.\n\n<h2>Enter Coolify: Heroku's open-source soulmate</h2>\n<p>The tool I have moved my entire stack to is Coolify.</p>\n<p>Coolify is an open-source, self-hostable alternative to Vercel, Heroku and Netlify. It gives you the same \"git push to deploy\" experience, but it runs on your own hardware.</p>\n<p>Whether you have a small ARM VPS on Hetzner (the CAX11 starts around €6/month) or a free ARM instance on Oracle Cloud, Coolify turns it into a private PaaS.</p>\n<h3>A mindset shift</h3>\n<p>I recently wrote about how <a href=\"https://ansezz.com/blog/coolify-docker-saas-hosting/\">Coolify and Docker are changing SaaS hosting</a>, but the shift goes deeper than a tool change. It is a move toward technical sovereignty.</p>\n<h3>Why Coolify works for engineers</h3>\n<ul>\n<li><strong>Zero vendor lock-in.</strong> Your configurations are stored on your server. If Coolify disappeared tomorrow, your Docker containers would keep running.</li>\n<li><strong>Automatic SSL.</strong> It handles Let's Encrypt out of the box. No more messing with Nginx configs or certbot.</li>\n<li><strong>Database management.</strong> You can spin up Postgres, MySQL, Redis or MongoDB in one click. They run as containers on your server, so you pay $0 in additional managed service fees.</li>\n<li><strong>Pull request deployments.</strong> It creates temporary environments for every PR, like Vercel, but without the \"team seat\" tax.</li>\n</ul>\n<figure>\n<h2>The magic of ARM (Graviton and OCI)</h2>\n<p>Once you have self-hosting in place, ARM is the multiplier that takes the bill from \"cheap\" to \"almost free.\" Stop renting x86 and start renting ARM.</p>\n<h3>What AWS says about Graviton</h3>\n<p>AWS says its Graviton instances cost up to 20% less than comparable x86 instances.</p>\n<h3>Oracle's Always Free tier</h3>\n<p>The real \"cheat code\" right now is Oracle Cloud Infrastructure (OCI). Its \"Always Free\" tier gives you 2 ARM Ampere A1 OCPUs and 12 GB of RAM at no cost, forever.</p>\n<p>(Oracle halved this from 4 OCPUs / 24 GB in mid-2026, so size accordingly.) I can run a small production workload (frontend, backend, database and Redis) on that single free instance using Coolify.</p>\n<h3>The math changes</h3>\n<p>When you pair ARM efficiency with a self-hosted orchestrator, the math changes. A startup paying $500/month on AWS can often move that entire workload to a sub-$15/month ARM instance on Hetzner, or to OCI's free tier outright.</p>\n<p>That difference goes straight back into your runway or your marketing budget.</p>\n<p>Rent ARM, run Coolify, and keep the difference as runway.</p>\n<h2>Docker and Nix: the engine room</h2>\n<p>Coolify relies heavily on Docker, which is the industry standard for a reason. It makes sure that what works on my machine works on the server.</p>\n<p>As I move deeper into the \"vibe coding\" era, I'm also looking at how technologies like Nix can make our environments even more stable.</p>\n<h3>Reproducible environments for agents</h3>\n<p>Using Nix flakes to define the development environment and Docker to package the runtime gives you a repeatable deployment pipeline.</p>\n<p>When I use tools like the <a href=\"https://ansezz.com/blog/mcp-context-aware-agents/\">Model Context Protocol (MCP)</a>, I want my AI agents to have a clear, reproducible environment to work in. Self-hosting doesn't mean \"unprofessional.\" It means total control over the stack.</p>\n<h2>Comparison: the hidden cost of \"easy\"</h2>\n<p>Let's look at the numbers for a standard Laravel or Node.js app with a database and a background worker.</p>\n<table>\n<thead>\n<tr>\n<th>Line item</th>\n<th>Managed path (Vercel + Supabase + AWS S3)</th>\n<th>Coolify path (Hetzner VPS)</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Hosting</td>\n<td>Vercel Pro: $20/month per developer seat</td>\n<td>4 vCPU ARM / 8GB RAM (CAX21): ~$12.50/month</td>\n</tr>\n<tr>\n<td>Database</td>\n<td>Supabase Pro: $25/month</td>\n<td>Included (container on the VPS)</td>\n</tr>\n<tr>\n<td>Storage / backups</td>\n<td>AWS S3 + bandwidth: $15/month</td>\n<td>Backups to S3-compatible storage: $1/month</td>\n</tr>\n<tr>\n<td>Platform</td>\n<td>Included</td>\n<td>Coolify: $0 (open source)</td>\n</tr>\n<tr>\n<td><strong>Total</strong></td>\n<td><strong>$60+/month (and rising with every user/teammate)</strong></td>\n<td><strong>~$13.50/month</strong></td>\n</tr>\n</tbody>\n</table>\n<h3>The gap keeps growing</h3>\n<p>The \"managed\" path costs roughly 4x more before you even have your first 100 users, and the gap widens with every teammate and every GB of egress. To run the numbers for your own stack, including the hours you will spend on upkeep, try my <a href=\"https://ansezz.com/tools/self-hosting-cost-calculator/\">self-hosting cost calculator</a>.</p>\n<p>The half hour it takes to install Coolify on a fresh Linux box is worth the thousands of dollars saved over the life of the project.</p>\n\nRebuild this table with your own invoices: list each managed service you pay for and the line it would become on one Coolify server. If the right column is under a third of the left, you have your business case.\n\n<h2>Practical steps to a self-hosted SaaS stack</h2>\n<p>If you are tired of the cloud tax, here is my recommended path to freedom:</p>\n<ol>\n<li><strong>Grab a VPS.</strong> I recommend Hetzner for raw performance per price, or OCI for its generous free tier. Pick an ARM-based instance (Ubuntu 24.04).</li>\n<li><strong>Install Coolify.</strong> Run the one-line install command from their documentation. It takes about 5 minutes.</li>\n<li><strong>Connect your Git.</strong> Link your GitHub or GitLab account.</li>\n<li><strong>Dockerize your app.</strong> If you are using Laravel, it is as simple as adding a <code>Dockerfile</code>. For Vite or Next.js, Coolify has built-in builders that don't even need a <code>Dockerfile</code>.</li>\n<li><strong>Move your DB.</strong> Export your managed DB and import it into a Coolify-managed container. Set up S3 backups immediately.</li>\n</ol>\n<figure>\n\nDo step 1 to 3 on a spare ARM instance this week with one small side project. Moving a real app is much easier once you have shipped a throwaway one.\n\n<h2>The bottom line</h2>\n<p>We are entering a cycle where efficiency matters more than ever. The days of \"VC-subsidized\" infrastructure are over. Whether you are building a small tool or a large enterprise SaaS, you owe it to your bottom line to look at self-hosting.</p>\n<h3>Who holds the keys</h3>\n<p>Coolify has matured to the point where the developer experience is close to the big players. The real difference is who owns the keys to the castle.</p>\n<p>When you do outgrow a single box, the platform scales with you. I cover that in <a href=\"https://ansezz.com/blog/scaling-with-coolify/\">advanced Coolify deployment strategies</a>.</p>\n\n- Cloud credits hide the real cost: idle databases, NAT gateways and complexity you don't need yet.\n- Coolify gives you git-push deploys, automatic SSL, one-click databases and PR previews on your own server.\n- ARM instances on Hetzner or Oracle's Always Free tier cut the bill further.\n- For a typical small app, the managed path costs roughly 4x more, and the gap grows with your team.\n- Start with one throwaway project, then move your real app and its database.\n\n<p>If your cloud bill is growing faster than your users, <a href=\"https://ansezz.com/services/\">here's how I help teams move to self-hosted Coolify on ARM</a>, or drop a note via <a href=\"https://ansezz.com/contact/\">contact</a>.</p>\n<p>What is the most \"expensive\" mistake you have ever made on a cloud bill: a forgotten NAT gateway or a runaway Lambda function?</p>\n</figure></figure>",
      "date_published": "2026-05-30T00:00:00.000Z",
      "date_modified": "2026-05-30T00:00:00.000Z",
      "tags": [
        "devops",
        "coolify",
        "self-hosting",
        "devops",
        "docker",
        "multi-tenancy"
      ],
      "image": "https://ansezz.com/blog/coolify-self-hosted-saas/hero-62ec4a84.webp"
    },
    {
      "id": "https://ansezz.com/blog/ml-vs-genai/",
      "url": "https://ansezz.com/blog/ml-vs-genai/",
      "title": "Machine learning vs generative AI",
      "summary": "Machine Learning predicts; Generative AI creates. The discriminative-vs-generative split decides your architecture, infra, and monthly cloud bill.",
      "content_html": "<p>Machine learning predicts. Generative AI creates. Pick the wrong one and you burn GPU credits on a model that hallucinates when it should be calculating.</p>\n\n- Traditional ML is discriminative: it learns `P(y | x)` from structured data to classify or predict, cheaply and precisely.\n- Generative AI learns how data is distributed so it can create text, code or images, at a much higher compute cost and with hallucination risk.\n- Strong production systems are hybrid: ML for scoring and routing, GenAI for the language layer users see.\n\n<p>Your data may be sitting idle in a warehouse while competitors ship agentic systems that write code and predict churn in the same stack.</p>\n<h3>Hype blurs the line</h3>\n<p>Marketing hype blurs the line between these two technologies, and stakeholders use the terms interchangeably. That mismatch breeds technical debt.</p>\n<p>Build a generative model for a task that demands strict mathematical precision and you set yourself up for failure. Rely on traditional models for creative automation and your product feels rigid and outdated.</p>\n<h3>Know the engineering difference</h3>\n<p>The fix is understanding the engineering differences between predictive and creative systems. If you want to zoom out first, my breakdown of <a href=\"https://ansezz.com/blog/ai-vs-machine-learning/\">AI vs machine learning</a> shows where both sit in the broader AI hierarchy.</p>\n<h2>Machine learning: the logic of prediction</h2>\n<p>Traditional Machine Learning (ML) is the backbone of modern data science. It is mainly a discriminative technology.</p>\n<p>Its job is to tell different types of data apart or to predict a number from historical patterns. You ask it to categorize an input or forecast a trend.</p>\n<h3>Built for structured data</h3>\n<p>ML models thrive on structured data. They look at rows and columns in a database to find correlations.</p>\n<p>For instance, a regression model might analyze thousands of Shopify transactions to predict next month's revenue. A classification model might look at server logs to spot a potential DDoS attack.</p>\n<h3>One model, one job</h3>\n<p>Traditional ML is often task-specific. If you want to detect fraud, you train a fraud detection model, and that model cannot suddenly start recommending products.</p>\n<p>It is a precision tool built for a single objective. That focus makes ML very efficient in high-stakes settings where accuracy and predictability are what matter.</p>\n<h2>Generative AI: the architecture of creation</h2>\n<p>Traditional ML analyzes data to make a choice; GenAI uses data to create something new.</p>\n<p>It is built on deep learning architectures, mainly Transformers and Diffusion models. Instead of only classifying data, these systems learn the probability distribution of their training set so they can generate new outputs.</p>\n<h3>Next-token prediction, coherent results</h3>\n<p>When you prompt a frontier Large Language Model (LLM) like Claude or GPT, you are using a generative system. It predicts the next most likely token in a sequence, and the result is coherent text or a code snippet.</p>\n<p>That creative ability is what makes GenAI so useful for building <a href=\"https://ansezz.com/blog/agentic-commerce-shopify/\">agentic commerce solutions on Shopify</a>.</p>\n<h3>Foundation models</h3>\n<p>GenAI models are often foundation models, trained on huge unstructured datasets with billions of parameters. They handle a wide range of tasks without being retrained from scratch.</p>\n<p>One model can summarize a legal document, write a Python script and brainstorm marketing copy. That flexibility is its main strength, and it also brings the risk of hallucinations.</p>\n<p>One kind of model draws the line between answers. The other writes answers that never existed before.</p>\n<figure>\n<h2>Machine learning vs generative AI: discriminative vs generative models</h2>\n<p>The core technical difference lies in their mathematical objectives. To know which one to deploy, look at how each processes information.</p>\n<h3>Discriminative models (traditional ML)</h3>\n<p>Discriminative models learn the boundary between classes. Mathematically, they model the conditional probability <code>P(y | x)</code>: \"given the input <code>x</code>, what is the probability of the label <code>y</code>?\"</p>\n<ul>\n<li><strong>Focus:</strong> telling data points apart.</li>\n<li><strong>Output:</strong> a discrete label (Spam/Not Spam) or a continuous value (Price).</li>\n<li><strong>Efficiency:</strong> usually needs less compute for inference.</li>\n<li><strong>Example:</strong> a Random Forest algorithm flagging fraudulent credit card transactions.</li>\n</ul>\n<h3>Generative models (GenAI)</h3>\n<p>Generative models learn how the data itself is distributed. They model the joint probability <code>P(x, y)</code> or the probability of the input itself <code>P(x)</code>: \"what does a typical example of this data look like?\"</p>\n<ul>\n<li><strong>Focus:</strong> understanding the structure of the data to reproduce it.</li>\n<li><strong>Output:</strong> new data samples (text, image, audio).</li>\n<li><strong>Efficiency:</strong> very resource-intensive, needing specialized GPUs.</li>\n<li><strong>Example:</strong> a Transformer model generating a new Laravel controller from a prompt.</li>\n</ul>\n<table>\n<thead>\n<tr>\n<th>Feature</th>\n<th>Machine Learning (Discriminative)</th>\n<th>Generative AI (Generative)</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Primary Goal</strong></td>\n<td>Classify or Predict</td>\n<td>Create New Content</td>\n</tr>\n<tr>\n<td><strong>Data Type</strong></td>\n<td>Structured (Tables, Logs)</td>\n<td>Unstructured (Text, Images)</td>\n</tr>\n<tr>\n<td><strong>Output Type</strong></td>\n<td>Numbers, Labels, Scores</td>\n<td>Content, Code, Media</td>\n</tr>\n<tr>\n<td><strong>Model Complexity</strong></td>\n<td>Low to Medium</td>\n<td>Very High</td>\n</tr>\n<tr>\n<td><strong>Training Data</strong></td>\n<td>Task-Specific, Labeled</td>\n<td>Massive, Unlabeled/Self-Supervised</td>\n</tr>\n</tbody>\n</table>\n<h2>Use cases: when to choose one over the other</h2>\n<p>The right tool depends on your business goals and your data. Using an LLM to predict a stock price is an expensive mistake. Using a linear regression model to write a blog post is impossible.</p>\n<h3>When to use machine learning</h3>\n<p>ML is the better fit for tasks that need high precision and deterministic logic. If you are working through <a href=\"https://ansezz.com/work/\">complex technical challenges in a real product</a>, ML is often the right choice for the \"back-office\" logic.</p>\n<ol>\n<li><strong>Fraud detection:</strong> spotting anomalies in financial transactions where false positives must be kept low.</li>\n<li><strong>Inventory forecasting:</strong> predicting stock levels for an e-commerce store from seasonal trends.</li>\n<li><strong>Recommendation engines:</strong> ranking products for a user based on their browsing history.</li>\n<li><strong>Medical diagnostics:</strong> analyzing lab results to flag specific health markers.</li>\n</ol>\n<h3>When to use generative AI</h3>\n<p>GenAI shines when you need to bridge human language and machine logic. It is a strong fit for intuitive interfaces and creative automation.</p>\n<ol>\n<li><strong>Code generation:</strong> automating boilerplate or converting legacy code to frameworks like Laravel and Vue.js.</li>\n<li><strong>Content personalization:</strong> generating unique product descriptions or email subject lines for thousands of customers.</li>\n<li><strong>Search and retrieval:</strong> using Retrieval-Augmented Generation (RAG) so users can \"chat\" with their own documentation. Learn more in my guide on <a href=\"https://ansezz.com/blog/7-rag-mistakes-production/\">common RAG mistakes</a>.</li>\n<li><strong>Prototyping:</strong> quickly generating UI mockups or synthetic data to test a new application.</li>\n</ol>\n\nList your AI ideas and mark each one \"predict a number or label\" or \"produce text, code or media.\" The first group is an ML job, the second a GenAI job.\n\n<h2>The hybrid future: combining ML and GenAI in production</h2>\n<p>In 2026, strong engineering teams don't choose one over the other. They build hybrid architectures, using traditional ML for the heavy data processing and GenAI for the user-facing interaction layer.</p>\n<h3>Support tickets</h3>\n<p>Consider a modern customer support system. An ML model runs sentiment analysis and routes the ticket to the right department based on urgency.</p>\n<p>Once routed, a GenAI agent drafts a response using internal knowledge bases. The system ends up both efficient and empathetic.</p>\n<h3>Churn prevention</h3>\n<p>Another example comes from <a href=\"https://ansezz.com/blog/ai-vs-traditional-development/\">AI vs traditional development</a>. An ML model predicts which users are likely to churn.</p>\n<p>A GenAI model then creates a custom discount offer and a personalized email tailored to that user's interests to keep them engaged.</p>\n\n  <div>An LLM reads every ticket, guesses urgency and writes the reply. Slow, expensive and inconsistent at triage.</div>\n  <div>A small ML model scores sentiment and urgency in milliseconds. The LLM only drafts replies for tickets that need one.</div>\n\n<figure>\n\nFind one place where an LLM makes a yes/no or scoring decision. Try a small classifier on the same data and compare cost, speed and accuracy.\n\n<h2>DevOps and deployment considerations</h2>\n<p>The two need different infrastructure strategies.</p>\n<h3>Traditional ML</h3>\n<p>Traditional ML models are often small enough to run on standard CPUs or even edge devices. They are easy to containerize with Docker and cheap to host on platforms like <a href=\"https://ansezz.com/blog/coolify-docker-saas-hosting/\">Coolify or AWS</a>.</p>\n<h3>Generative AI</h3>\n<p>GenAI models are a different beast. Even \"small\" LLMs need significant VRAM, and the <a href=\"https://ansezz.com/blog/training-vs-inference/\">cost profile of training vs inference</a> shapes every hardware decision.</p>\n<p>If you self-host, you need solid GPU orchestration. Many businesses use API-based options like Claude or OpenAI to avoid managing hardware. For teams concerned with data privacy or high volume, deploying open-weights models on Google Cloud or AWS with specialized inference servers is the standard approach.</p>\n<h3>Monitor the right failure mode</h3>\n<p>Whichever you choose, keep the architecture clean. Containerize for environment consistency and monitor the right failure mode: model drift in ML, hallucination rates in GenAI.</p>\n<p>That rigor keeps your AI solutions scalable and maintainable over time.</p>\n\n- **Define your goal first:** use ML for prediction and classification, GenAI for content creation and reasoning.\n- **Data type matters:** ML excels with structured, tabular data. GenAI is built for unstructured data like text, images and code.\n- **Cost and latency:** traditional ML is usually cheaper and faster to run. GenAI needs much more compute and has higher latency.\n- **Hybrid works best:** combine the precision of ML with the flexibility of GenAI.\n- **Plan infrastructure early:** use containers and cloud-native services for each technology's hardware needs.\n- **Avoid the hype:** don't use an LLM because it is trending. Pick the model that solves the engineering problem most efficiently.\n\n<p>If you're deciding where ML ends and GenAI begins in your product, <a href=\"https://ansezz.com/services/\">here's how I help teams design and ship hybrid AI features</a>.</p>\n<p>Is your current data strategy focused on predicting what will happen next, or are you ready to start creating the outcomes you want to see?</p>\n</figure></figure>",
      "date_published": "2026-05-30T00:00:00.000Z",
      "date_modified": "2026-05-30T00:00:00.000Z",
      "tags": [
        "ai",
        "production",
        "ai-engineering",
        "machine-learning",
        "llm",
        "rag",
        "architecture",
        "ai"
      ],
      "image": "https://ansezz.com/blog/ml-vs-genai/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/laravel-octane-high-traffic/",
      "url": "https://ansezz.com/blog/laravel-octane-high-traffic/",
      "title": "Laravel Octane for high-traffic PHP apps",
      "summary": "Laravel Octane boots your app once and keeps it warm in memory, feeding requests through a worker pool. How to cut boot overhead and scale to high traffic.",
      "content_html": "<p>Standard PHP rebuilds your whole Laravel app for every request. Octane builds it once and keeps it warm.</p>\n\n- PHP-FPM boots the framework on every request, so a complex app can spend 30ms to 50ms on setup before your code even runs.\n- Octane keeps the app in memory on FrankenPHP, Swoole, Open Swoole or RoadRunner, and feeds requests through a worker pool you size for CPU-bound or I/O-bound work.\n- Long-lived workers make leaks dangerous, so avoid request data in statics and singletons, and use `--max-requests` to recycle workers.\n\n<p>Standard PHP is a bit like a restaurant that fires its entire staff and rebuilds the kitchen for every customer order. You walk in, they hire a chef, buy a stove, cook your meal and then demolish the building as soon as you leave.</p>\n<p>It's consistent and safe, but it wastes a lot of energy when you're trying to serve thousands of people at once.</p>\n<h3>The PHP-FPM lifecycle</h3>\n<p>Every request boots the entire Laravel framework, loads your service providers, parses your config and instantiates your objects. For low-traffic sites, that's fine.</p>\n<p>At real scale, say a busy <a href=\"https://ansezz.com/blog/laravel-multi-tenancy/\">multi-tenant SaaS</a>, those milliseconds of \"boot time\" become a wall you can't climb without throwing expensive hardware at it. Horizontal scaling buys you room, but it doesn't fix the underlying latency.</p>\n<h3>Stop rebuilding the kitchen</h3>\n<p>Laravel Octane boots your application once, keeps it in memory and feeds requests to it through a fast worker pool. PHP goes from \"short-lived script\" to \"long-lived process.\"</p>\n<h2>Why your app feels heavy without Laravel Octane</h2>\n<p>The overhead of traditional PHP costs you efficiency as well as speed. In a standard request, your CPU spends a big chunk of time just getting the application ready to work.</p>\n<p>Once it finally starts, it runs the database query, renders the view and then dies.</p>\n<h3>Paying for setup again and again</h3>\n<p>If you run a complex Laravel monolith with dozens of packages and custom service providers, your boot time might be 30ms to 50ms before a single line of business logic runs.</p>\n<p>Under high traffic, this leads to CPU thrashing. You're paying for the \"setup\" over and over again.</p>\n<h3>Warm workers</h3>\n<p>Octane removes this boot cycle. With application servers like Swoole or RoadRunner, your app stays resident in memory.</p>\n<p>The first request boots the framework, and later requests hit a \"warm\" application. You can move from 50ms responses to sub-10ms responses just by changing how the process is managed.</p>\n\n  <div>Every request boots Laravel, loads providers and parses config, runs your code, then throws it all away.</div>\n  <div>The app boots once per worker. Each request goes straight to your code on an app that is already warm.</div>\n\n<figure>\n<h2>Choosing your application server</h2>\n<p>Octane runs on top of an application server, and it supports four: FrankenPHP, Swoole, Open Swoole and RoadRunner.</p>\n<p>FrankenPHP (a Go-based server built on Caddy) is the first option in the Octane docs, and it's the easiest path to HTTP/2, HTTP/3 and automatic HTTPS. In production, the two I reach for most are Swoole and RoadRunner, and they solve the \"persistent state\" problem differently.</p>\n<h3>Swoole</h3>\n<p>Swoole is a C extension for PHP. It's a fast networking engine that lets PHP handle asynchronous tasks, coroutines and long-lived connections.</p>\n<ul>\n<li><strong>Pros:</strong> it is very fast. Because it lives as an extension, it has deep access to PHP's internals. It also enables the Octane cache, an in-memory store (backed by Swoole tables) that the docs clock at up to 2 million reads/writes per second, plus concurrent tasks, ticks and intervals.</li>\n<li><strong>Cons:</strong> it can be a bit of a nightmare to install and debug. As a binary extension, you have to compile it or find the right package for your OS. Xdebug doesn't always play nice with it, and it can be picky about your environment.</li>\n</ul>\n<h3>RoadRunner</h3>\n<p>RoadRunner is written in Go. It acts as a load balancer and process manager that talks to your PHP workers over a fast binary protocol (Goridge).</p>\n<ul>\n<li><strong>Pros:</strong> no extensions required. It's a single binary you drop into your project. It's much easier to set up in a <a href=\"https://ansezz.com/blog/coolify-docker-saas-hosting/\">Docker container</a> and feels more \"cloud-native.\"</li>\n<li><strong>Cons:</strong> it's slightly slower than Swoole because of the communication overhead between the Go binary and the PHP processes, though for most web apps, where time goes to the database and other I/O, you won't notice the difference.</li>\n</ul>\n<h3>Which one to pick</h3>\n<table>\n<thead>\n<tr>\n<th>Server</th>\n<th>Install effort</th>\n<th>Pick it when</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>FrankenPHP</strong></td>\n<td>Smoothest setup</td>\n<td>You're starting out and want HTTP/3 for free</td>\n</tr>\n<tr>\n<td><strong>RoadRunner</strong></td>\n<td>Single Go binary</td>\n<td>You want no PHP extension to manage</td>\n</tr>\n<tr>\n<td><strong>Swoole</strong></td>\n<td>C extension, fiddly</td>\n<td>You need every millisecond, task workers or Octane cache</td>\n</tr>\n</tbody>\n</table>\n<p>For most developers starting out, I'd pick FrankenPHP at the install prompt and not overthink it. Reach for Swoole only when you're chasing every last millisecond or need its concurrency features like async task workers, ticks and the Octane cache.</p>\n<h2>Tuning your worker pool</h2>\n<p>The secret sauce of Octane is the \"worker pool.\" Instead of one process per request, you have a fixed number of workers waiting for incoming traffic.</p>\n<p>If you misconfigure it, you'll either leave performance on the table or crash your server. My <a href=\"https://ansezz.com/tools/capacity-calculator/\">server capacity calculator</a> turns requests per second and latency into the number of workers you need.</p>\n<h3>CPU-bound vs I/O-bound</h3>\n<p>The right pool size depends on whether your app is CPU-bound or I/O-bound:</p>\n<ol>\n<li><strong>CPU-bound apps:</strong> if you do heavy data processing or image manipulation, set your worker count to your number of CPU cores. More workers just cause context switching and slow things down.</li>\n<li><strong>I/O-bound apps:</strong> most web apps spend 90% of their time waiting for a database, Redis or an external API. Here you can scale workers to 2x or even 4x your core count, so one worker can wait on the DB while another handles a new request.</li>\n</ol>\n<pre><code># Starting Octane with 16 workers for an I/O-heavy app\nphp artisan octane:start --server=swoole --workers=16\n</code></pre>\n<h3>Task workers</h3>\n<p>Don't forget <strong>task workers</strong> if you're using Swoole. Sized separately with the <code>--task-workers</code> flag, they run in their own processes.</p>\n<p>They're a good fit for fanning out concurrent queries without blocking the main request cycle.</p>\n\nCheck whether your app is CPU-bound or I/O-bound under load, then start Octane with workers equal to your cores (CPU-bound) or 2x your cores (I/O-bound) and compare P95 latency.\n\n<h2>The danger of persistent state</h2>\n<p>The main hurdle when moving to Octane is the shift in mindset. In traditional PHP, \"leaky\" code barely matters because the process dies after 100ms.</p>\n<p>Under Octane, a leak doesn't die with the request. It waits and grows.</p>\n<h3>What to watch</h3>\n<p>If a service provider has a static array that you append to on every request, that array grows until your server runs out of RAM. Be careful with:</p>\n<ul>\n<li><strong>Static properties:</strong> don't use them for request-specific data.</li>\n<li><strong>Singletons:</strong> a singleton you register in the app container stays alive. If it caches data, make sure that data is cleared or managed between requests.</li>\n<li><strong>Global state:</strong> avoid <code>global</code> variables at all costs (which you should be doing anyway).</li>\n</ul>\n<p>Laravel \"resets\" some core services between requests, but it can't catch everything. If you're migrating an old codebase from <a href=\"https://ansezz.com/blog/monolith-to-microservices/\">monolith to microservices</a>, audit your service providers for any long-lived state.</p>\n<figure>\n<h3>Protecting yourself with max-requests</h3>\n<p>The best insurance against memory leaks is the <code>--max-requests</code> flag. It tells Octane to gracefully restart a worker after it has handled a set number of requests.</p>\n<p>Octane already does this every 500 requests by default. Tune the number to match how fast your workers accumulate memory.</p>\n<pre><code># Restart workers every 1000 requests to prevent memory bloat\nphp artisan octane:start --max-requests=1000\n</code></pre>\n<p>This keeps memory use predictable while still giving you the speed of a warm application.</p>\n\nSearch your service providers for `static` properties and singletons that store request data. Move that data into the request or clear it between requests before you switch to Octane.\n\n<h2>Real-world optimization tips</h2>\n<p>Once Octane is running, a few technical levers can squeeze out more performance.</p>\n<ol>\n<li><strong>Database connections:</strong> in Octane your workers stay alive, so each one holds its own DB connection open between requests instead of reconnecting every time. That's a win, but watch the math: more workers means more open connections. Check your <code>database.php</code> config and make sure the worker count doesn't blow past your DB server's max connection limit. This matters even more once you add <a href=\"https://ansezz.com/blog/replication-vs-backup-laravel/\">read replicas</a>.</li>\n<li><strong>Octane cache:</strong> if you're using Swoole, reach for <code>Cache::store('octane')</code>. It's an in-memory Swoole table that's very fast and shared across every worker on the box. Use it for frequently accessed configuration or small datasets that rarely change. Remember it's wiped when the server restarts.</li>\n<li><strong>Bytecode caching:</strong> make sure OPcache is enabled and tuned. Set <code>opcache.validate_timestamps=0</code> in production since your code won't change while the server is running.</li>\n<li><strong>Graceful reloads:</strong> when you deploy new code, run <code>php artisan octane:reload</code>. It gracefully restarts the workers without dropping current connections, which you need for zero-downtime deployments.</li>\n</ol>\n\nAdd `php artisan octane:reload` as the last step of your deploy script, and check that `workers x app servers` stays below your database's max connections. My [Postgres connection pool calculator](/tools/postgres-connection-calculator/) does that sum for you and suggests PgBouncer settings.\n\n\n- **Identify the bottleneck:** only use Octane if boot time is the problem. If your database queries take 2 seconds, Octane won't help.\n- **Test for leaks:** watch how your app behaves under sustained load and profile memory growth over time.\n- **Monitor workers:** keep an eye on CPU and RAM to find the right worker count.\n- **Use concurrency:** on Swoole, `Octane::concurrently()` runs independent operations in parallel via task workers and returns their results together, cutting total response time for complex pages.\n\n<p>Octane lets PHP compete with Node.js and Go for high-concurrency apps while keeping the Laravel developer experience. If you're moving a busy Laravel app to Octane, <a href=\"https://ansezz.com/services/\">here's how I help teams tune workers, connections and deploys for high traffic</a>, or drop a note via <a href=\"https://ansezz.com/contact/\">contact</a>.</p>\n<p>Are you running Octane in production yet, or is the fear of memory leaks keeping you on PHP-FPM?</p>\n</figure></figure>",
      "date_published": "2026-05-29T00:00:00.000Z",
      "date_modified": "2026-05-29T00:00:00.000Z",
      "tags": [
        "laravel",
        "laravel",
        "performance",
        "scaling"
      ],
      "image": "https://ansezz.com/blog/laravel-octane-high-traffic/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/shopify-ucp-quick-start/",
      "url": "https://ansezz.com/blog/shopify-ucp-quick-start/",
      "title": "Shopify UCP quick-start: make your store agent-ready",
      "summary": "AI agents are becoming buyers, and they look for a UCP profile before they can check out. A quick-start to making your Shopify store agent-ready.",
      "content_html": "<p>AI agents now shop for people inside ChatGPT, Google AI Mode, Gemini, Copilot and Meta. Before an agent can check out on your store, it reads one JSON file at <code>/.well-known/ucp</code>.</p>\n\n- The Universal Commerce Protocol (UCP) is an open standard, co-developed by Shopify and Google, that tells AI agents what your store sells and how to check out.\n- On Shopify, agentic storefronts are on by default for eligible stores, and Shopify publishes your UCP profile for you. Check it under Sales channels &gt; Agentic.\n- Headless and custom stacks often break discovery by not passing `/.well-known/ucp` through, so open that URL and confirm you get JSON back.\n\n<p>Your store can be invisible to a fast-growing group of buyers: AI agents acting for shoppers.</p>\n<p>If an agent can't find a machine-readable profile for your Shopify store, it can still mention your products, but it can't complete a purchase for the shopper.</p>\n<h3>Built for agents with wallets</h3>\n<p>Traditional SEO was built for humans with eyeballs. UCP is built for agents with wallets.</p>\n<p>The rollout is moving fast, and every new AI channel that adopts UCP can sell your products without a new integration. Here is how to make sure your store is ready.</p>\n<h2>Why classic SEO falls short</h2>\n<p>For ten years we tuned meta tags and JSON-LD schema. That was enough when Google was a list of links. We've now moved into <a href=\"https://ansezz.com/blog/agentic-commerce-shopify/\">agentic commerce on Shopify</a>.</p>\n<h3>Three questions an agent asks</h3>\n<p>Agents don't browse your collections or look at hero banners. They want three answers:</p>\n<ol>\n<li>What do you sell?</li>\n<li>Can I buy it right now?</li>\n<li>Which API do I call to check out?</li>\n</ol>\n<p>If your site doesn't answer these in a standard way, the agent moves on to a store that does. Product schema tells an agent what a product <em>is</em>. UCP tells it how to <em>transact</em>.</p>\n<table>\n<thead>\n<tr>\n<th>Layer</th>\n<th>Classic SEO and schema</th>\n<th>UCP profile</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Audience</strong></td>\n<td>Search crawlers and humans</td>\n<td>AI shopping agents</td>\n</tr>\n<tr>\n<td><strong>Describes</strong></td>\n<td>Pages and products</td>\n<td>Services, capabilities and payment handlers</td>\n</tr>\n<tr>\n<td><strong>Lives at</strong></td>\n<td>Page markup</td>\n<td><code>/.well-known/ucp</code></td>\n</tr>\n<tr>\n<td><strong>Lets the reader</strong></td>\n<td>Rank and display products</td>\n<td>Build a cart and complete checkout</td>\n</tr>\n</tbody>\n</table>\n<figure>\n<h2>What is the Universal Commerce Protocol?</h2>\n<p>UCP is an open standard co-developed by Shopify and Google. Shopify lists backers including Amazon, American Express, Etsy, Mastercard, Meta, Microsoft, Salesforce, Stripe, Target, Walmart and Visa.</p>\n<p>It defines a handshake between a business and an AI agent, covering cart creation, checkout, payment and post-purchase.</p>\n<h3>The profile at <code>/.well-known/ucp</code></h3>\n<p>The core of the handshake is a JSON profile at <code>/.well-known/ucp</code>. When an agent reaches your domain, it fetches this file to learn what you support, then negotiates the capabilities you have in common.</p>\n<p>The spec requires <code>version</code>, <code>services</code> and <code>payment_handlers</code> inside the <code>ucp</code> object, and <code>capabilities</code> is optional. Versions are dates; the latest stable release is 2026-08-25. Shopify's own Admin and Storefront APIs use dated versions too, released every quarter, and my <a href=\"https://ansezz.com/tools/shopify-api-versions/\">Shopify API version calendar</a> shows which ones are still supported.</p>\n<p>Here is a trimmed version of the profile a live Shopify store serves today:</p>\n<pre><code>{\n  \"ucp\": {\n    \"version\": \"2026-08-25\",\n    \"services\": {\n      \"dev.ucp.shopping\": [\n        {\n          \"version\": \"2026-08-25\",\n          \"transport\": \"mcp\",\n          \"endpoint\": \"https://your-store.myshopify.com/api/ucp/mcp\",\n          \"schema\": \"https://ucp.dev/2026-08-25/services/shopping/mcp.openrpc.json\"\n        }\n      ]\n    },\n    \"capabilities\": {\n      \"dev.ucp.shopping.checkout\": [\n        {\n          \"version\": \"2026-08-25\",\n          \"schema\": \"https://ucp.dev/2026-08-25/schemas/shopping/checkout.json\"\n        }\n      ],\n      \"dev.ucp.shopping.cart\": [{ \"version\": \"2026-08-25\" }],\n      \"dev.ucp.shopping.catalog.search\": [{ \"version\": \"2026-08-25\" }]\n    },\n    \"payment_handlers\": {\n      \"dev.shopify.shop_pay\": [{ \"id\": \"shop_pay\", \"version\": \"2026-04-08\" }],\n      \"com.google.pay\": [{ \"id\": \"gpay\", \"version\": \"2026-01-11\" }]\n    }\n  }\n}\n</code></pre>\n<p>The agent learns which capabilities you support and where to send each request. No scraping, and no guessing where the add-to-cart button is.</p>\n<p>If an agent can't read your checkout, it shops somewhere else.</p>\n<h2>How to enable UCP on Shopify</h2>\n<p>On a standard Shopify or Shopify Plus store, you don't write this JSON by hand. Shopify generates and hosts the profile, and agentic storefronts are active by default for eligible stores.</p>\n<h3>Check the Agentic channel</h3>\n<p>Open <strong>Sales channels &gt; Agentic</strong> in your Shopify admin. <strong>Allow Shopify to manage for me</strong> is on by default. It shares your products through Shopify Catalog, turns on direct checkout where it's supported and enrolls you in new AI channels automatically.</p>\n<p>Turn it off if you want to control Shopify Catalog access, direct checkout and auto-enrollment yourself, channel by channel. For Google AI Mode and Gemini, products also come through the Google &amp; YouTube channel or Google Merchant Center.</p>\n<h3>Headless stores</h3>\n<p>On Hydrogen or a custom frontend, your domain may not serve Shopify's profile at all. Make sure your edge or middleware passes <code>/.well-known/ucp</code> through to Shopify.</p>\n<p>I often see agentic discovery break because Vercel or Netlify rewrites, or a catch-all route, swallow the <code>.well-known</code> directory and return the homepage instead.</p>\n\n  <div>The headless app's catch-all route answers every path, so `/.well-known/ucp` returns the homepage HTML with a 200. The agent gets no profile.</div>\n  <div>A rewrite sends `/.well-known/ucp` to the store's myshopify domain before the catch-all runs, and the agent receives the JSON profile.</div>\n\n\nOpen the Agentic channel in your Shopify admin and write down which AI channels are on, and whether direct checkout is active for each one. Decide on purpose whether you want \"Allow Shopify to manage for me\" left on.\n\n<h2>Why this matters now</h2>\n<p>Search is shifting from links to agents. Shopify's agentic storefronts already reach ChatGPT, Google AI Mode and Gemini, Microsoft Copilot and Meta, and when a new AI platform adopts UCP, Shopify stores are ready to sell there with no extra integration.</p>\n<p>This sits next to the Model Context Protocol work covered in <a href=\"https://ansezz.com/blog/mcp-context-aware-agents/\">MCP context-aware agents</a>. UCP even lists MCP as one of its transports, which is how the profile above exposes the shopping service.</p>\n<h3>The black box problem</h3>\n<p>If your profile is missing, your store is a black box. An agent might see your products in a search index, but it can't fulfill \"buy me the best hiking boots from a local store.\" It picks a store where it can confirm checkout right away.</p>\n<figure>\n<h2>Auditing your implementation</h2>\n<p>Don't flip a switch and hope. Confirm the profile is reachable from outside.</p>\n<h3>A two-minute check</h3>\n<ol>\n<li>Open a private browser window.</li>\n<li>Go to <code>https://yourdomain.com/.well-known/ucp</code>.</li>\n<li>You should see a raw JSON object with a <code>ucp</code> key.</li>\n<li>If you get a 404, HTML or a redirect to your homepage, your theme, app or proxy is blocking the path.</li>\n</ol>\n<p>Check <code>robots.txt</code> too, and make sure you aren't disallowing the <code>.well-known</code> directory.</p>\n<h3>Custom backends</h3>\n<p>If a custom Laravel backend sits in front of your store, add an explicit route or proxy rule for this path. I often see middleware strip hidden directories for \"security\" reasons. Here, that rule only costs you sales.</p>\n\nRun `curl -s https://yourdomain.com/.well-known/ucp` from a terminal and pipe it into my [JSON formatter](/tools/json-formatter/). If the output is HTML or an error, fix the route before you touch anything else.\n\n\n- **Confirm agentic storefronts** under Sales channels &gt; Agentic in your Shopify admin.\n- **Verify `/.well-known/ucp`** returns valid JSON on your own domain, not only on myshopify.com.\n- **Check the payment handlers** in the profile (for example Shop Pay and Google Pay) match what you expect.\n- **Fix headless rewrites** so the `.well-known` path reaches Shopify.\n- **Check your proxy configs** if you host custom middleware, for example on [Coolify Docker SaaS hosting](/blog/coolify-docker-saas-hosting/), so the path isn't dropped.\n\n<p>Commerce is moving from a web built for clicks to one built for agents, and UCP is the first real step toward making your store a first-class seller there. If your store is headless or runs custom middleware, <a href=\"https://ansezz.com/services/\">here's how I help Shopify brands get agent-ready</a>.</p>\n<p>Are you letting agents shop your store yet, or are you still building only for people who click?</p>\n</figure></figure>",
      "date_published": "2026-05-28T00:00:00.000Z",
      "date_modified": "2026-05-28T00:00:00.000Z",
      "tags": [
        "shopify",
        "shopify",
        "agentic-commerce",
        "ai",
        "hydrogen",
        "agentic-ai"
      ],
      "image": "https://ansezz.com/blog/shopify-ucp-quick-start/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/claude-mcp-dev-tools/",
      "url": "https://ansezz.com/blog/claude-mcp-dev-tools/",
      "title": "Claude MCP: connecting my dev tools to LLMs",
      "summary": "The Model Context Protocol is a USB-C port for LLMs: one MCP server, any host, no MxN integration tax. The architecture, the servers I run, and why it is safe.",
      "content_html": "<p>Custom integrations for every tool are a maintenance tax that never ends. The Model Context Protocol (MCP) replaces them with one plug.</p>\n\n- Without a standard, M AI apps and N data sources need M x N custom integrations. MCP turns that into one server per tool that any compatible host can use.\n- An MCP setup has three parts: the host the user works in, the client that speaks the protocol, and the server that exposes resources, tools and prompts.\n- Local servers talk over `stdio` and remote servers use OAuth 2.1 with PKCE, so you can give each tool only the access it needs.\n\n<p>Every time I build a custom integration for a new tool, I add one more thing to maintain forever. We have reached a point where building the product is often faster than setting up the pipes to connect it to our data.</p>\n<p>That is the exact problem Claude's MCP (Model Context Protocol) was built to kill.</p>\n<h3>A smart model in a room with no windows</h3>\n<p>If you have spent any time building <a href=\"https://ansezz.com/blog/agentic-workflows-vibe-coding/\">agentic workflows and vibe coding</a>, you know the feeling. You have a very capable LLM like Claude, but it is locked in a room with no windows.</p>\n<p>To give it context, you copy-paste code, export CSV files, or spend three days writing a brittle wrapper for a third-party API just so your assistant can \"see\" your work.</p>\n<h3>The architect who can't visit the site</h3>\n<p>This fragmentation is a major bottleneck in modern software development. Our models are capable, but they are cut off from our local files, our databases and our production logs.</p>\n<p>It is like having a world-class architect who isn't allowed to visit the construction site. They are guessing based on the photos you decide to send them. Enter the Model Context Protocol, or in Anthropic's own framing, a USB-C port for AI applications.</p>\n<h2>The fragmentation tax on your productivity</h2>\n<p>The problem is simple. Every AI application (Claude Desktop, a custom agent, an IDE extension) wants to talk to your data. On the other side, every data source (your GitHub repos, your Postgres databases, your Slack channels) has its own API and authentication flow.</p>\n<h3>The M x N problem</h3>\n<p>Without a standard, we are stuck in an M×N problem. If you have 5 AI apps and 10 data sources, you need 50 different integrations.</p>\n<p>(If you are weighing whether to expose a plain REST endpoint or an MCP server here, I broke that down in <a href=\"https://ansezz.com/blog/api-vs-mcp/\">API vs MCP</a>.)</p>\n<p>This is why most \"AI-powered\" tools feel shallow. They support a few basic integrations, and if you want to use your internal company data, you are back to writing custom glue code.</p>\n<h3>What it costs you</h3>\n<p>The cost is real. We waste hours rebuilding the same connectors, every new integration is another potential leak, and the \"magic\" of AI evaporates the moment we hit a data silo.</p>\n\nWrite down every AI tool your team uses and every data source it needs. Multiply the two numbers. That is how many integrations you would maintain without a shared protocol.\n\n<h2>MCP as a universal standard</h2>\n<p>Claude MCP (Model Context Protocol) is a serious, open attempt to standardize how AI applications discover and use data and tools. Instead of building a connector for every model and every tool, you build an MCP server.</p>\n<h3>The server as a translator</h3>\n<p>This server sits between your data and the AI and exposes a consistent interface that any MCP-compliant host (like Claude Desktop) can understand.</p>\n<p>It works like the USB standard. It doesn't matter if you plug in a mouse, a keyboard or an external drive. The protocol is the same, so it just works.</p>\n<p>This changes how you build <a href=\"https://ansezz.com/blog/mcp-context-aware-agents/\">context-aware agents</a>. Instead of hard-coding logic into the agent, you \"plug in\" the servers you need.</p>\n\n  <div>Each AI app gets its own connector for each data source. Five apps and ten sources means fifty integrations to build and secure.</div>\n  <div>Each data source gets one MCP server. Any MCP host can discover and use it, so you add a tool once.</div>\n\n<figure>\n<h3>How the architecture works</h3>\n<p>There are three main players in the MCP ecosystem:</p>\n<ol>\n<li><strong>The host.</strong> This is the environment the user works in: Claude Desktop, a terminal or an IDE like Cursor. The host manages the lifecycle of the connection.</li>\n<li><strong>The client.</strong> This is the part of the host that speaks the protocol. It asks the server what it can do. Since the <code>2026-07-28</code> spec there is no up-front handshake: each request carries its own protocol version and capabilities.</li>\n<li><strong>The server.</strong> This is a lightweight program that provides context (resources), actions (tools) and prompt templates.</li>\n</ol>\n<h3>A local example</h3>\n<p>Say I want Claude to read my local project files. I run a local MCP server that exposes those files as \"resources.\"</p>\n<p>The host (Claude Desktop) asks the server: \"what do you have?\" The server replies: \"I have these 10 files and a tool to run grep searches.\"</p>\n<p>The model can then call the \"grep\" tool whenever it needs to find a function definition. I didn't write a single line of logic inside Claude to make that happen. I connected the server.</p>\n<h2>Modularity and the MCP server ecosystem</h2>\n<p>Once a server is built, anyone can use it. The community has already built servers for almost everything. Here are a few I use in my daily workflow:</p>\n<ul>\n<li><strong>A Postgres server.</strong> I point Claude at a local or remote database so it can inspect schemas and run read-only queries to help me debug data issues without leaving the chat. Anthropic's original reference Postgres server is now archived, so I lean on a maintained community build.</li>\n<li><strong>GitHub's official MCP server.</strong> It lets the model search my repositories, list issues and open pull requests. It is like having a junior dev who knows where the code is. GitHub now ships and hosts this itself, with a remote option you authenticate via OAuth.</li>\n<li><strong>A Google Drive server.</strong> Handy when I need to cross-reference technical docs stored in Drive with the implementation in my IDE. The reference server is archived too, so I treat it as a starting point rather than something to trust blindly.</li>\n</ul>\n<h3>From chatting to delegating</h3>\n<p>Wiring these up is the step that separates chatting with a model from delegating to one. It is the point where a workflow crosses into <a href=\"https://ansezz.com/blog/ai-coding-workflow-levels/\">the agentic levels</a> instead of staying a faster autocomplete.</p>\n<p>It also helps with <a href=\"https://ansezz.com/blog/agentic-commerce-shopify/\">agentic commerce for Shopify</a>. Imagine an agent that talks to your Shopify store via MCP to check inventory levels or update product descriptions in real time, over a secure, standard connection.</p>\n<p>Build the connector once. Every host that speaks MCP gets it for free.</p>\n\nBefore you write a server, search the [MCP Registry](https://registry.modelcontextprotocol.io/) for the tool you need and check whether the vendor maintains an official one.\n<p>When you are ready to add a server to Claude Desktop, Claude Code, Cursor or VS Code, my <a href=\"https://ansezz.com/tools/mcp-config-generator/\">MCP config generator</a> writes the config file and checks the one you already have.\n</p><p></p>\n<h2>Security first: the sandbox model</h2>\n<p>The first question I get when I talk about connecting dev tools to an LLM is: \"is it safe?\"</p>\n<p>Security is a first-class concern in MCP's design. The server runs as a separate process that only exposes the tools and resources you grant it.</p>\n<p>(Note: a local stdio server still runs with your normal user privileges. It is process isolation, not a hardened sandbox, so only run servers you trust.)</p>\n<h3>Local servers: a narrow pipe</h3>\n<p>For local servers, the protocol typically uses <code>stdio</code> (stdin/stdout). The server can only talk to the host through that narrow pipe. It has no open network ports listening for connections, and it only exists while the host is running it.</p>\n<h3>Remote servers: scoped OAuth</h3>\n<p>For remote servers, MCP standardizes on OAuth 2.1 (with PKCE) for authorization. That allows fine-grained, scope-based permissions. You can authorize a GitHub MCP server to read only public repositories, or a database server to access only specific tables.</p>\n<h3>Least privilege for AI tools</h3>\n<p>This is a big step up from the \"give me your master API key\" approach we have seen in the past. You can treat AI tools with the same least-privilege mindset you use for any other service in your stack.</p>\n<p>That matters when you are trying to avoid <a href=\"https://ansezz.com/blog/7-rag-mistakes-production/\">RAG mistakes in production</a>, where data leakage is a top-tier risk.</p>\n<figure>\n<h2>Why I am betting on MCP</h2>\n<p>I have seen plenty of \"standards\" come and go. What makes MCP different is its simplicity and its backing.</p>\n<p>Anthropic made it open source, and in December 2025 donated it to the Agentic AI Foundation, a Linux Foundation fund, so no single vendor controls it.</p>\n<h3>Where this is heading</h3>\n<p>We are moving toward \"agentic\" software development. In this world, AI does more than write snippets of code. It acts as an orchestrator that can reach into your cloud infrastructure on GCP, check your Docker logs and suggest fixes for a failing Laravel app.</p>\n<p>Without a protocol like MCP, that vision is too expensive and too risky to build at scale. With MCP, tools become plug-and-play.</p>\n<h3>How to start</h3>\n<p>If you are ready to experiment, here is what I recommend:</p>\n<ul>\n<li><strong>Install the Claude Desktop app.</strong> It is one of the most mature MCP hosts, and an easy place to start.</li>\n<li><strong>Try the filesystem server.</strong> This is the easiest way to see it work. Give Claude access to a specific folder and watch it navigate your codebase.</li>\n<li><strong>Don't build, search first.</strong> Browse the official MCP Registry before writing anything. The reference repo now keeps only a handful of maintained servers (filesystem, fetch, git, memory and a few others), and most well-known integrations are either archived or maintained by the vendor directly, like GitHub's own server.</li>\n<li><strong>Think in tools as well as prompts.</strong> Ask what \"tools\" your internal systems could expose. If you have a custom admin panel, could it be an MCP server?</li>\n</ul>\n\nConnect the filesystem server to one project folder in Claude Desktop and ask it to find where a specific function is defined. Note how many copy-paste steps that saved you.\n\n\n- MCP replaces M x N custom integrations with one server per tool.\n- Host, client and server each have one job, and the server decides what it exposes.\n- Local servers use `stdio`; remote servers use OAuth 2.1 with PKCE and scoped permissions.\n- Only run servers you trust: a local server runs with your user privileges.\n- Search the MCP Registry before you build, and prefer vendor-maintained servers.\n- Connecting your tools cuts the switching between tabs, terminals and docs, so you stay in flow longer.\n\n<p>If you want your internal tools and dev stack exposed to Claude through scoped MCP servers, <a href=\"https://ansezz.com/services/\">here's how I help teams build and secure them</a>, or <a href=\"https://ansezz.com/contact/\">get in touch</a> directly.</p>\n<p>What is the one internal tool you wish you could \"plug in\" to Claude right now?</p>\n</figure></figure>",
      "date_published": "2026-05-27T00:00:00.000Z",
      "date_modified": "2026-05-27T00:00:00.000Z",
      "tags": [
        "ai",
        "mcp",
        "claude",
        "agentic-ai",
        "llm"
      ],
      "image": "https://ansezz.com/blog/claude-mcp-dev-tools/hero-e0213e82.webp"
    },
    {
      "id": "https://ansezz.com/blog/redis-semantic-caching-rag/",
      "url": "https://ansezz.com/blog/redis-semantic-caching-rag/",
      "title": "Caching for speed: Redis and semantic layers in RAG",
      "summary": "Stop paying for the same LLM call twice. Two-tier caching with Redis keys and RedisVL semantic lookups slashes RAG latency and trims your LLM API bill.",
      "content_html": "<p>The cheapest LLM call is the one you never make. A two-tier Redis cache answers repeat questions in milliseconds and keeps them off your API bill.</p>\n\n- Put an exact-match cache first (normalized, hashed query keys) and a semantic cache second (RedisVL vector lookup with a strict distance threshold).\n- Guard against the semantic trap: version your context, partition by tenant and invalidate on data changes.\n- Measure hit rate and semantic precision, and review the borderline hits every week before you loosen the threshold.\n\n<p>You shipped your RAG pipeline. Retrieval is accurate and the LLM is quick. Then you look at your cloud bill and your P99 latency.</p>\n<p>Every query, even \"what are your shipping times?\" for the tenth time, runs the full chain: embedding, vector search and an expensive LLM call.</p>\n<h3>Paying twice for the same answer</h3>\n<p>At scale you pay for the same computation over and over. Users wait two seconds for answers that could take twenty milliseconds, and your \"denial of wallet\" risk climbs.</p>\n<p>The fix is a smarter cache. Semantic caching with Redis turns a multi-second chain into a single-digit-millisecond lookup. At the hit rates a busy FAQ bot reaches, it can cut LLM API spend by roughly half. Provider prompt caching saves on a different part, the long prefix you resend on every call, and my <a href=\"https://ansezz.com/tools/prompt-caching-calculator/\">prompt caching calculator</a> shows when that pays off.</p>\n<h2>The two-tier cache</h2>\n<p>Standard caching relies on exact matches. \"How do I reset my password?\" and \"how do i reset my password\" can share a key if you normalize the string. \"Can you help me change my password?\" misses a traditional cache.</p>\n<h3>Tier 1: exact cache</h3>\n<p>A plain key-value entry in Redis. I normalize the query (lowercase, trim, strip punctuation) and hash it. My <a href=\"https://ansezz.com/tools/hash-generator/\">SHA hash generator</a> shows what those keys look like.</p>\n<p>It's the first line of defense: almost free, with zero false positives.</p>\n<h3>Tier 2: semantic cache</h3>\n<p>If the exact cache misses, I embed the query and look for a close match in a Redis vector index. If a previous question is close enough (a cosine distance of 0.1 or less, about 0.9 similarity), I serve the cached answer instead of calling the LLM.</p>\n<p>You never do the heavy lifting twice for the same intent.</p>\n<figure>\n<h2>Why Redis for semantic caching</h2>\n<p>Most developers think of Redis as a key-value store. With the <a href=\"https://redis.io/blog/how-to-cache-semantic-search/\">Redis Vector Library (RedisVL)</a>, it also works as a fast vector index.</p>\n<p>Why not use your main vector DB, like Pinecone or Weaviate? Latency.</p>\n<h3>Small index, close to the app</h3>\n<p>Your main vector DB is tuned to search millions of document chunks. The semantic cache is much smaller: recent queries and their answers. Redis usually already sits next to your application, so you skip network hops.</p>\n<p>I typically see Redis vector lookups finish in under 5 ms. An embedding API call takes around 100 ms and an LLM generation around 1,500 ms.</p>\n<table>\n<thead>\n<tr>\n<th>Path</th>\n<th>Typical latency</th>\n<th>LLM cost</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Exact cache hit</strong></td>\n<td>A few ms</td>\n<td>None</td>\n</tr>\n<tr>\n<td><strong>Semantic cache hit</strong></td>\n<td>Embedding plus under 5 ms lookup</td>\n<td>None</td>\n</tr>\n<tr>\n<td><strong>Full RAG pipeline</strong></td>\n<td>1.5 to 2+ seconds</td>\n<td>Full call</td>\n</tr>\n</tbody>\n</table>\n<p>Every cache hit is an LLM call you don't wait for and don't pay for.</p>\n<h2>Implementing the semantic layer</h2>\n<p>The key setting is the similarity threshold. Too loose and you give users wrong answers (the \"semantic trap\"). Too strict and you never get a hit.</p>\n<p>I start with a distance threshold of 0.1. RedisVL measures Redis cosine distance on a 0 to 2 scale, where lower is stricter, so 0.1 is roughly 90% similarity.</p>\n<pre><code>from redisvl.extensions.cache.llm import SemanticCache\n\n# Initialize the cache with a conservative threshold and a 4-hour TTL\nllm_cache = SemanticCache(\n    name=\"production_rag_cache\",\n    redis_url=\"redis://localhost:6379\",\n    distance_threshold=0.1,\n    ttl=14400,\n)\n\ndef answer(query: str) -&gt; str:\n    hit = llm_cache.check(prompt=query)\n    if hit:\n        return hit[0][\"response\"]\n\n    response = run_rag_pipeline(query)\n    llm_cache.store(prompt=query, response=response)\n    return response\n</code></pre>\n<p>This wrapper embeds the incoming query, runs the vector search in Redis and returns the closest cached response when one is within the threshold.</p>\n\nExport a week of user questions and count exact duplicates after normalizing them. That number is your exact-cache hit rate before you write any semantic code.\n\n<h2>Avoid the semantic trap</h2>\n<p>Semantic caching is useful but risky if you aren't careful. When your underlying data changes, the cache can keep serving old, wrong answers.</p>\n<h3>Version your context</h3>\n<p>I include a <code>context_version</code> in cache keys or metadata. When I re-index the product catalog or update the docs, I bump the version. Old entries stop matching and the next request rebuilds them with fresh data.</p>\n<h3>Isolate tenants</h3>\n<p>If User A asks \"what is my balance?\", you can't serve that answer to User B. I partition the cache:</p>\n<ul>\n<li><strong>Namespaces:</strong> <code>cache:tenant_id:query_hash</code></li>\n<li><strong>Metadata filters:</strong> add <code>tenant_id</code> as a filterable field on the vector index.</li>\n</ul>\n<p>Semantic matches then only happen inside the right security boundary. My post on <a href=\"https://ansezz.com/blog/laravel-multi-tenancy/\">Laravel multi-tenancy</a> uses the same isolation principles.</p>\n\n  <div>\"What is my balance?\" from one tenant matches the cached answer from another tenant. Close in meaning, wrong customer.</div>\n  <div>Every lookup filters on `tenant_id` and the current `context_version`, so a hit can only come from the same tenant and the same data.</div>\n\n<h2>TTL and staleness</h2>\n<p>In a standard cache you set a 3,600-second expiry and forget it. With a semantic cache I use tiers:</p>\n<ul>\n<li><strong>Exact matches:</strong> 1-hour TTL. Same question, same answer.</li>\n<li><strong>Semantic matches:</strong> 4-hour TTL, with a \"last validated\" timestamp. These cost more to produce, so I keep them longer.</li>\n<li><strong>Proactive invalidation:</strong> when a Shopify product price changes, a Redis worker purges every entry tied to that product ID.</li>\n</ul>\n<h3>Watch the sliding TTL</h3>\n<p>RedisVL refreshes the TTL on an entry every time <code>check()</code> matches it. A popular answer can live forever on its own, which is why versioning and proactive invalidation matter more than the TTL.</p>\n<p>I've written about similar <a href=\"https://ansezz.com/blog/event-driven-pubsub/\">event-driven patterns</a> for handling these updates at scale.</p>\n<figure>\n<h2>Measuring success</h2>\n<p>Don't turn on the cache and walk away. Track two numbers:</p>\n<ol>\n<li><strong>Cache hit rate:</strong> the share of queries Redis answers. I aim for 30 to 50% on general FAQ-style bots.</li>\n<li><strong>Semantic precision:</strong> whether the cached answers are actually correct.</li>\n</ol>\n<h3>Review the borderline hits</h3>\n<p>I log every semantic hit with its distance. Once a week I sample the hits closest to the threshold (distance between 0.07 and 0.1) and review them by hand. If too many are different questions that only look alike, I tighten the threshold.</p>\n\nAdd the distance score to your semantic hit logs today. Next week, read 20 hits from the band just inside your threshold and mark each one right or wrong.\n\n\n- **Install `redisvl`** and set up a basic vector index in your dev environment.\n- **Look up in two tiers:** exact first, then semantic.\n- **Start strict:** a distance threshold of 0.05 to 0.1.\n- **Partition and version:** add `tenant_id` and `context_version` to avoid cross-talk and stale answers.\n- **Measure:** watch hit rate, precision and your API bill.\n\n<p>For more on production RAG, see <a href=\"https://ansezz.com/blog/7-rag-mistakes-production/\">7 RAG mistakes in production</a> or browse the <a href=\"https://ansezz.com/blog/category/architecture/\">architecture archive</a>. If you want a cache like this in front of your own pipeline, <a href=\"https://ansezz.com/services/\">here's how I help teams cut RAG latency and LLM spend</a>, or <a href=\"https://ansezz.com/contact/\">drop me a note</a>.</p>\n<p>Which query in your system keeps hitting the LLM when it shouldn't?</p>\n</figure></figure>",
      "date_published": "2026-05-26T00:00:00.000Z",
      "date_modified": "2026-05-26T00:00:00.000Z",
      "tags": [
        "architecture",
        "redis",
        "ai",
        "rag",
        "vector-search",
        "performance",
        "infrastructure"
      ],
      "image": "https://ansezz.com/blog/redis-semantic-caching-rag/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/smart-auto-scaling-ai/",
      "url": "https://ansezz.com/blog/smart-auto-scaling-ai/",
      "title": "Smart auto-scaling for modern AI apps",
      "summary": "CPU auto-scaling fails GPU workloads. Why queue depth, KV-cache pressure, and TTFT beat CPU as triggers, plus the KEDA patterns to scale AI in time.",
      "content_html": "<p>When CPU sits at 20% and inference requests time out, your autoscaler is watching the wrong number. Scale AI workloads on queue depth, KV-cache pressure and time to first token.</p>\n\n- CPU tracks tokenization and HTTP handling; the real work happens on the GPU, so a CPU-based autoscaler stays calm while users wait.\n- Waiting requests, KV-cache usage and TTFT are leading signals, and vLLM exports all three as Prometheus metrics you can feed to KEDA.\n- Add a forecast for predictable peaks so capacity is warm before the spike, since a new replica can take a minute or more to load model weights.\n\n<p>Your AI app is lagging and users are complaining, but the cloud dashboard says everything is fine. CPU hovers at a comfortable 20% while inference requests time out.</p>\n<h3>The scaling trap</h3>\n<p>Traditional auto-scaling was built for web servers, where CPU and memory are the bottlenecks. For LLM inference, those numbers tell you very little.</p>\n<p>If you wait for CPU to hit 80% before adding a pod, the service is in trouble long before the second instance finishes booting. GPU-bound workloads need a different playbook.</p>\n<h3>Scale on pressure</h3>\n<p>To run a cost-effective AI SaaS, move past reactive hardware metrics. Scale on queue pressure, cache pressure and how fast users get their first token.</p>\n<h2>Why CPU-based auto-scaling misleads you</h2>\n<p>The Kubernetes Horizontal Pod Autoscaler (HPA) is most often set up to watch CPU utilization. For a Laravel or Node.js API, that works: more requests mean more CPU cycles.</p>\n<h3>The work moved to the GPU</h3>\n<p>AI models are different. The CPU handles tokenization, request routing and HTTP headers. The heavy lifting happens on the GPU.</p>\n<p>I've seen production clusters where the GPU is pinned at 100% while the CPU sits idle. The autoscaler sees low CPU, decides the pods are fine and never adds a replica, while the queue keeps growing.</p>\n<figure>\n<h2>GPU utilization vs occupancy</h2>\n<p>When you start monitoring GPUs, you meet two confusing metrics: utilization and occupancy.</p>\n<h3>Utilization is a duty cycle</h3>\n<p>GPU utilization is the percentage of time the GPU was running work during a sample period. It lags. By the time it reads 90%, your request queue may have been building for a while.</p>\n<h3>Occupancy is finer-grained</h3>\n<p>Occupancy measures how many warps are active on each streaming multiprocessor (SM) compared with the maximum. You can have high utilization and low occupancy if your batch size is too small.</p>\n<p>For scaling, treat utilization as a baseline and look at what happens before a request even reaches the GPU.</p>\n<h2>Queue depth: your best leading indicator</h2>\n<p>To stop fires before they start, watch queue depth. In vLLM it's the number of requests waiting for a slot in the engine, exported as <code>vllm:num_requests_waiting</code>. SGLang exposes a similar metric.</p>\n<h3>Scale before latency climbs</h3>\n<p>Queue depth predicts latency. If your model handles 16 concurrent requests before P99 latency starts to climb, set the scaling trigger at 12. To get a real P99 from your own response times, paste them into my <a href=\"https://ansezz.com/tools/percentile-calculator/\">latency percentile calculator</a>.</p>\n<p>Scaling on queue depth adds capacity while the current hardware is still inside your SLO. That buys the head start you need to pull a fresh container and load 20 GB of model weights into GPU memory.</p>\n<table>\n<thead>\n<tr>\n<th>Signal</th>\n<th>vLLM metric</th>\n<th>What it tells you</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Waiting requests</strong></td>\n<td><code>vllm:num_requests_waiting</code></td>\n<td>Work is piling up before the engine</td>\n</tr>\n<tr>\n<td><strong>KV-cache usage</strong></td>\n<td><code>vllm:kv_cache_usage_perc</code></td>\n<td>How close you are to preemptions</td>\n</tr>\n<tr>\n<td><strong>Preemptions</strong></td>\n<td><code>vllm:num_preemptions</code></td>\n<td>Requests are being evicted and recomputed</td>\n</tr>\n<tr>\n<td><strong>Time to first token</strong></td>\n<td><code>vllm:time_to_first_token_seconds</code></td>\n<td>What users feel when they hit enter</td>\n</tr>\n<tr>\n<td><strong>CPU utilization</strong></td>\n<td>(node metric)</td>\n<td>Tokenization and HTTP load, rarely the bottleneck</td>\n</tr>\n</tbody>\n</table>\n<p>In Prometheus, the counter shows up as <code>vllm:num_preemptions_total</code> and the TTFT histogram as <code>vllm:time_to_first_token_seconds_bucket</code>.</p>\n<p>Watch the line of waiting requests. It moves before the chip looks busy.</p>\n\nGraph `vllm:num_requests_waiting` next to your P99 latency for one busy day. Find the queue length where latency starts to climb, and set your scale-out threshold about 25% below it.\n\n<h2>Token velocity and the KV cache</h2>\n<p>In generative AI, requests vary a lot in size. A 10-token summary is light. A 4,000-token RAG analysis is heavy.</p>\n<h3>When the cache fills up</h3>\n<p>The KV cache is the GPU memory that stores attention keys and values for in-flight sequences. When it's nearly full, the engine preempts a request to free blocks.</p>\n<p>In vLLM V1, the default preemption mode is recompute: the evicted request's cache is dropped, and its prompt plus generated tokens are processed again when it resumes. Latency jumps, and P99 starts to look like a mountain range.</p>\n<h3>Scale on two numbers</h3>\n<p>I recommend scaling on a combination of:</p>\n<ol>\n<li><strong>Token velocity:</strong> total tokens per second across all active instances.</li>\n<li><strong>KV-cache pressure:</strong> the share of cache blocks in use (<code>vllm:kv_cache_usage_perc</code>).</li>\n</ol>\n<p>When the cache is full, low GPU utilization doesn't help. You can't fit more work on that chip, so you add replicas.</p>\n\n  <div>The HPA watches CPU at 20% and does nothing. The KV cache fills, requests get preempted and recomputed, and TTFT climbs past two seconds.</div>\n  <div>KEDA watches waiting requests and KV-cache usage. A new replica starts loading weights while the current one is still inside its TTFT target.</div>\n\n<h2>Predictive scaling with ARIMA</h2>\n<p>Reactive scaling always plays catch-up. Even with fast boots, there's a delay. For apps with predictable traffic, I use ARIMA (Auto-Regressive Integrated Moving Average) models to forecast load.</p>\n<h3>Warm before the spike</h3>\n<p>If traffic always spikes at 9:00 am on Mondays, I don't wait for the queue to grow. A time-series forecast brings up the base-load pods at 8:55 am.</p>\n<p>You still pay for what you use, and the capacity is there before the first user clicks \"Generate.\"</p>\n<figure>\n<h2>Practical steps for your stack</h2>\n<p>Here is how I structure it:</p>\n<ul>\n<li><strong>Use KEDA.</strong> The Kubernetes Event-Driven Autoscaler can scale on Prometheus queries, like waiting requests or P99 latency, instead of CPU alone.</li>\n<li><strong>Set TTFT SLOs.</strong> Time to first token shapes how fast the app feels. If TTFT P99 passes 500 ms, you need more replicas.</li>\n<li><strong>Use a composite signal.</strong> Don't rely on one metric. Combine queue depth, cache pressure and GPU utilization. Once you have several replicas, route with <a href=\"https://ansezz.com/blog/gpu-aware-load-balancing/\">GPU-aware load balancing</a> so traffic lands on the least-loaded GPU.</li>\n<li><strong>Check your RAG pipeline.</strong> Sometimes the scaling issue is a retrieval issue. Slow vector search makes the engine wait and holds GPU slots. These <a href=\"https://ansezz.com/blog/7-rag-mistakes-production/\">common RAG mistakes</a> help you rule out an upstream bottleneck.</li>\n<li><strong>Handle retries in the app.</strong> For Shopify apps or custom SaaS, make sure your <a href=\"https://ansezz.com/blog/agentic-workflows-vibe-coding/\">agentic workflows</a> retry gracefully while capacity scales up.</li>\n</ul>\n\nWrite a KEDA ScaledObject with a Prometheus trigger on `vllm:num_requests_waiting`, apply it in staging and run a load test. Compare how long it takes to add a replica against your current CPU-based HPA.\n\n\n- **CPU is the wrong trigger** for GPU inference. Watch the queue and the cache.\n- **Queue depth leads latency.** Scale before your SLO breaks, not after.\n- **KV-cache pressure** predicts preemptions and the latency spikes they cause.\n- **Use KEDA** to scale on vLLM's Prometheus metrics, and track TTFT as the user-facing SLO.\n- **Forecast known peaks** so replicas finish loading weights before traffic arrives.\n\n<p>Smarter triggers save money on idle GPUs and spare your users the \"thinking...\" spinner. If you're running inference on Kubernetes, <a href=\"https://ansezz.com/services/\">here's how I help teams scale AI workloads on the right signals</a>, or <a href=\"https://ansezz.com/contact/\">drop me a note</a>.</p>\n<p>Are you still scaling on CPU, or have you moved to queue-based triggers yet?</p>\n</figure></figure>",
      "date_published": "2026-05-25T00:00:00.000Z",
      "date_modified": "2026-05-25T00:00:00.000Z",
      "tags": [
        "architecture",
        "scaling",
        "ai",
        "llm",
        "kubernetes",
        "llm-inference",
        "infrastructure"
      ],
      "image": "https://ansezz.com/blog/smart-auto-scaling-ai/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/gpu-aware-load-balancing/",
      "url": "https://ansezz.com/blog/gpu-aware-load-balancing/",
      "title": "GPU-aware load balancing for AI inference",
      "summary": "Round-robin breaks when LLM requests span 50 to 50,000 tokens. GPU-aware load balancing with prefill/decode disaggregation and the metrics that cut P99.",
      "content_html": "<p>Your load balancer says CPU is at 40% and everything is fine. Your GPUs are maxed out and your P99 latency is in the gutter.</p>\n\n- LLM requests vary wildly in weight, so round-robin and CPU metrics hide where the real load is.\n- Prefill (reading the prompt) is compute-bound and decode (writing tokens) is memory-bound, so they compete when mixed on one GPU.\n- Route on token queue depth, KV cache use, TTFT and inter-token latency, and send shared prefixes to the GPU that already has them cached.\n\n<p>You just scaled your RAG application to a hundred concurrent users. Suddenly, your latency spikes.</p>\n<p>Some users get their answers in two seconds, while others stare at a loading spinner for thirty. This is the problem GPU-aware load balancing exists to solve.</p>\n<h3>Models are not web servers</h3>\n<p>You are treating your AI models like traditional web servers. Sending a 4,000-token prompt to the same GPU that is generating a 50-token summary is a recipe for disaster.</p>\n<p>Round-robin routing is a relic of the past for LLM inference. If you don't account for how GPUs handle compute and memory, you waste money and hurt your user experience at the same time.</p>\n<h3>More GPUs won't fix it alone</h3>\n<p>The fix is a load balancer that understands what is happening inside the model. That means GPU-aware routing, prefill vs decode disaggregation, and treating your KV cache as one of the most valuable assets in your stack.</p>\n<h2>Why round-robin is a trap for LLMs</h2>\n<p>In traditional software, a request is a request. Whether it's a <code>GET /users</code> or a <code>POST /orders</code>, the variance in resource use is usually small and predictable.</p>\n<p>Standard load balancers like Nginx or HAProxy work great here. They look at basic health checks and send traffic to the next available worker.</p>\n<h3>Requests have very different weights</h3>\n<p>AI is different. One user might ask \"what is 2+2?\" while another uploads a 50-page PDF and asks for a deep analysis.</p>\n<p>If your load balancer sends both to the same GPU, the heavy request hogs the compute, and the light request waits in a queue.</p>\n<h3>CPU metrics tell you nothing</h3>\n<p>A GPU can be at 100% utilization while doing very different types of work. Some work is compute-bound and needs raw processing power.</p>\n<p>Other work is memory-bound, limited by how fast data moves in and out of VRAM. To route well, you have to look inside the inference lifecycle.</p>\n<h2>Prefill vs decode: the performance gap</h2>\n<p>LLM inference happens in two distinct phases. Understanding the difference is the \"aha!\" moment for GPU load balancing.</p>\n<h3>Prefill: read the whole prompt</h3>\n<p>In <strong>prefill</strong>, the model reads your entire prompt and processes all the tokens at once. It is a heavy, compute-intensive task that builds the <strong>KV cache</strong> (key-value cache).</p>\n<p>Prefill loves big batches and fast tensor cores. It is where the heavy lifting happens.</p>\n<h3>Decode: one token at a time</h3>\n<p>In <strong>decode</strong>, the model generates the response one token at a time. Each new token only needs the previous tokens and the KV cache.</p>\n<p>This phase is light on compute but very heavy on memory bandwidth. It is slow and long-lived.</p>\n<h3>Why mixing them hurts</h3>\n<p>When you mix the two on one GPU without a smart scheduler, the prefill of a new request often pauses the decode of existing requests. That causes the jittery, stuttering text users hate.</p>\n<p>GPU-aware load balancing lets you treat these phases differently across your fleet.</p>\n<table>\n<thead>\n<tr>\n<th>Phase</th>\n<th>What it does</th>\n<th>Bound by</th>\n<th>Metric to watch</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Prefill</strong></td>\n<td>Reads the full prompt at once</td>\n<td>Compute</td>\n<td>Time to first token (TTFT)</td>\n</tr>\n<tr>\n<td><strong>Decode</strong></td>\n<td>Writes the answer token by token</td>\n<td>Memory bandwidth</td>\n<td>Inter-token latency (ITL)</td>\n</tr>\n</tbody>\n</table>\n<figure>\n<h2>Metrics for the real world</h2>\n<p>To build a better router, stop looking at CPU and start looking at these four metrics:</p>\n<ol>\n<li><strong>Token queue depth:</strong> how many tokens are waiting to be processed? This shows \"load\" far better than request counts.</li>\n<li><strong>KV cache utilization:</strong> GPUs have limited VRAM, and the KV cache holds the \"memory\" of ongoing conversations. If a GPU's VRAM is 90% full of KV cache, it cannot accept a large new prompt, even if it looks \"idle.\"</li>\n<li><strong>Time to first token (TTFT):</strong> the latency of the prefill phase. If TTFT is climbing, your prefill pool is congested.</li>\n<li><strong>Inter-token latency (ITL):</strong> the speed of the decode phase. If this is high, your GPUs are likely limited by memory bandwidth.</li>\n</ol>\n<h3>Get the numbers from your engine</h3>\n<p>I often recommend tools like <a href=\"https://github.com/vllm-project/vllm\">vLLM</a> because they expose these metrics out of the box.</p>\n<p>You can pipe them into a custom gateway that routes on real-time VRAM availability instead of a simple \"is the server up?\" check.</p>\n\nScrape TTFT, inter-token latency and KV cache usage from one inference node and put them on a single dashboard next to your P99. Watch which one moves first when latency spikes.\n\n<h2>Prefix-aware routing: reuse the KV cache</h2>\n<p>The most expensive part of a RAG request is often re-processing the same system prompt or long context again and again.</p>\n<p>If you send five follow-up questions about the same document to five different GPUs, each GPU runs prefill for that document from scratch. On hosted APIs, prompt caching attacks the same cost, and my <a href=\"https://ansezz.com/tools/llm-cost-calculator/\">LLM cost calculator</a> shows the effect on your monthly bill.</p>\n<h3>Send the prompt where the cache is warm</h3>\n<p><strong>Prefix-aware routing</strong> (also called KV-cache-aware routing) fixes this. Your load balancer tokenizes the start of the prompt and looks for a GPU that already has that content in its KV cache.</p>\n<p>Matching the prefix lets you skip prefill for the shared part and reuse the cached keys and values directly. That can turn hundreds of milliseconds of TTFT into a near-instant first token.</p>\n<p>Every repeated prefill is a GPU redoing work it already finished somewhere else.</p>\n<h3>A common RAG blind spot</h3>\n<p>This is one of the most effective optimizations in a production RAG system. I've written before about <a href=\"https://ansezz.com/blog/7-rag-mistakes-production/\">common RAG mistakes</a>, and ignoring cache locality is one of them.</p>\n<p>For the layer above this (caching whole responses and embeddings), see <a href=\"https://ansezz.com/blog/redis-semantic-caching-rag/\">Redis and semantic caching for RAG</a>.</p>\n<h2>Splitting the fleet into specialized pools</h2>\n<p>As you scale, stop treating every GPU as a generalist. Build <strong>disaggregated inference fleets</strong> instead.</p>\n<p>I like to split my GPUs into two pools:</p>\n<ul>\n<li><strong>The prefill pool:</strong> compute-heavy GPUs (like H100s or B200s) tuned to chew through large prompts fast. These nodes process the initial context, then hand off the KV state.</li>\n<li><strong>The decode pool:</strong> GPUs sized for memory bandwidth and capacity rather than raw FLOPs (think A100s or H200s) that focus on producing tokens for existing requests.</li>\n</ul>\n<h3>Scale the pool your users stress</h3>\n<p>If your users upload huge documents but ask for short summaries, scale your prefill pool. If they have long, chatty conversations, scale your decode pool.</p>\n<p>This is the same logic used in <a href=\"https://ansezz.com/blog/coolify-docker-saas-hosting/\">modern DevOps with Coolify</a>. You wouldn't put your heavy database on the same tiny instance as your frontend, so why mix heavy prefill work with light decode work?</p>\n\n  <div>Every GPU does prefill and decode. New long prompts pause ongoing answers, and you can only scale everything at once.</div>\n  <div>Prefill GPUs read prompts and hand off the KV state. Decode GPUs keep streaming tokens, and you scale each pool for the load it actually gets.</div>\n\n<figure>\n<h2>Implementing your first GPU-aware router</h2>\n<p>You don't need to build a custom engine from scratch. Here is the practical path I follow when setting this up for a new SaaS:</p>\n<ol>\n<li><strong>Centralize your metrics:</strong> use Prometheus to scrape vLLM or TGI metrics from every GPU node.</li>\n<li><strong>Use a smart gateway:</strong> write a middleware in Go or Rust (or even a heavy-duty Lua script in OpenResty) that checks these metrics before choosing a target.</li>\n<li><strong>Prioritize KV cache:</strong> route on prefix overlap when you can. A cheap first approximation is session stickiness: if a node served this <code>conversation_id</code> recently and isn't at 100% KV utilization, send the follow-up there so the warm cache gets reused.</li>\n<li><strong>Set hard limits:</strong> if a GPU reaches 85% VRAM usage, take it out of the rotation for new prompts until some sessions finish.</li>\n</ol>\n\nAdd session stickiness on `conversation_id` to your gateway, with a fallback to the least-loaded node when the sticky one is above your VRAM limit.\n\n<h3>From black box to context-aware</h3>\n<p>Managing AI compute means moving from \"black box\" infrastructure to \"context-aware\" infrastructure. When your load balancer knows the difference between a 10-token greeting and a 10,000-token context window, your costs go down and your users stay happy.</p>\n<p>It's easy to get lost in the hype of \"agentic systems\" and <a href=\"https://ansezz.com/blog/mcp-context-aware-agents/\">context-aware agents</a>, but none of that matters if your infrastructure is buckling under unoptimized routing.</p>\n\n- **Round-robin fails for LLMs** because request weight varies from a few tokens to whole documents.\n- **Prefill is compute-bound, decode is memory-bound.** Mixing them on one GPU causes stutter.\n- **Route on token queue depth, KV cache use, TTFT and ITL,** not CPU.\n- **Prefix-aware routing reuses the KV cache** and cuts time to first token.\n- **Split prefill and decode pools** so you can scale each for your real workload.\n\n<p>If round-robin is hurting your inference latency, <a href=\"https://ansezz.com/services/\">here's how I help teams build GPU-aware routing for LLM serving</a>, or drop a note via <a href=\"https://ansezz.com/contact/\">contact</a>.</p>\n<p>If you are still using round-robin for your AI models, what is the main bottleneck you are seeing in your P99 latency right now?</p>\n</figure></figure>",
      "date_published": "2026-05-24T00:00:00.000Z",
      "date_modified": "2026-05-24T00:00:00.000Z",
      "tags": [
        "architecture",
        "llm-inference",
        "networking",
        "ai",
        "llm",
        "infrastructure"
      ],
      "image": "https://ansezz.com/blog/gpu-aware-load-balancing/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/circuit-breakers-vector-db/",
      "url": "https://ansezz.com/blog/circuit-breakers-vector-db/",
      "title": "Circuit breakers: stopping vector DB failures",
      "summary": "A slow vector DB kills SaaS faster than a dead one. The circuit-breaker pattern for AI infra: states, fallback tiers, and Laravel-friendly wiring.",
      "content_html": "<p>A slow vector database can take your whole app down without ever going offline. A circuit breaker is the switch that stops that from happening.</p>\n\n- In production, a slow dependency is often more dangerous than a dead one, because waiting requests eat your workers, memory and connections.\n- A circuit breaker has three states (closed, open, half-open) and fails fast once a failure threshold is hit, so the struggling service can recover.\n- Pair the breaker with fallbacks (keyword search, a Redis cache, LLM-only mode) and monitor how often it trips.\n\n<p>You built a beautiful RAG pipeline. It works perfectly on your machine with a few hundred vectors. Then you launch, traffic spikes and your managed vector database starts sweating.</p>\n<p>A similarity search that used to take 50ms now takes 5 seconds. Your web workers are all tied up waiting for responses that aren't coming.</p>\n<h3>Slow is the new down</h3>\n<p>The database isn't technically down, but it is slow enough to kill your entire application. Your users see spinning loaders until the request finally times out.</p>\n<p>This is a classic cascading failure (the kind a circuit breaker exists to stop), and it is the fastest way to drain your \"innovation budget\" and your users' patience.</p>\n<h3>Why waiting hurts</h3>\n<p>We often treat external APIs and databases as if they are always healthy. We write code that assumes the vector DB will return results. When it doesn't, we wait, and while we wait we hold onto memory and CPU cycles.</p>\n<p>The fix is an old-school electrical engineering concept applied to software: the circuit breaker. This guide shows you how to wrap your AI infrastructure in protective logic so a slow dependency doesn't take your whole SaaS down with it.</p>\n<h2>How the circuit breaker pattern works</h2>\n<p>The circuit breaker pattern is a state machine that sits between your application code and your external service.</p>\n<p>The idea comes straight out of Michael Nygard's <em>Release It!</em>. It monitors every call you make and has three states that dictate how it handles traffic.</p>\n<table>\n<thead>\n<tr>\n<th>State</th>\n<th>What happens to calls</th>\n<th>Moves on when</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Closed</td>\n<td>Calls flow through while the breaker counts failures</td>\n<td>The failure threshold is hit, then it opens</td>\n</tr>\n<tr>\n<td>Open</td>\n<td>Calls fail fast or get a fallback, no network call</td>\n<td>The cooldown ends, then it goes half-open</td>\n</tr>\n<tr>\n<td>Half-open</td>\n<td>A few test calls go through</td>\n<td>Tests pass (closed) or fail (open again)</td>\n</tr>\n</tbody>\n</table>\n<h3>Closed: the healthy state</h3>\n<p>In the closed state, the circuit is complete. Requests flow through to your vector database or LLM provider normally.</p>\n<p>The breaker is silently watching. It keeps a count of how many requests failed or took too long. As long as the failure rate stays below your threshold, it stays closed. This is the \"everything is fine\" mode.</p>\n<h3>Open: the fail-fast state</h3>\n<p>Once the failure threshold is hit (say 50% of requests failed in the last 30 seconds), the breaker \"trips\" and moves to the open state.</p>\n<p>Now every call to the vector DB immediately throws an error or returns a fallback response, without even attempting the network call. This gives your database room to breathe and recover, and your application stops wasting time on a service that is clearly struggling.</p>\n<h3>Half-open: the recovery test</h3>\n<p>After a cooldown period, the breaker moves to the half-open state and lets a small number of \"test\" requests through.</p>\n<p>If these test calls succeed, the breaker assumes the service is healthy again and moves back to closed. If they fail, it goes straight back to open for another cooldown cycle. This is a controlled way to probe the system before fully re-engaging.</p>\n<figure>\n<h2>Why your RAG pipeline needs this</h2>\n<p>RAG pipelines are particularly vulnerable because they usually involve several high-latency network hops. You embed the query, search the vector DB, then call the LLM. If any of these pieces fail or slow down, the whole experience breaks.</p>\n<h3>Hard errors are the easy part</h3>\n<p>Most developers only handle hard errors like a <code>404</code> or a <code>500</code> status code. But in production, \"slow\" is often more dangerous than \"down.\"</p>\n<p>A slow vector DB creates a bottleneck that backs up your entire request queue. By the time you notice, your server is out of memory because it is holding open thousands of connections.</p>\n<p>A dead service fails fast. A slow one quietly holds every worker hostage.</p>\n<p>If you have read my post on <a href=\"https://ansezz.com/blog/7-rag-mistakes-production/\">7 RAG mistakes in production</a>, you know that reliability is the difference between a demo and a product. The circuit breaker is your insurance policy against these outages.</p>\n\nList every external call in one RAG request (embedding API, vector DB, LLM) and write down its current timeout. Any call with no timeout, or one longer than your page budget, is your first breaker candidate.\n\n<h2>Implementing fallback strategies</h2>\n<p>Tripping the breaker shouldn't always mean showing an error message to the user. Good AI systems use fallbacks to keep some level of service even when parts of the stack are failing.</p>\n<h3>Hot and cold tiers</h3>\n<p>Think of your vector DB as your \"hot\" knowledge tier. If it fails, you should have a \"cold\" fallback, such as a standard keyword search in your primary Postgres or MySQL database.</p>\n<p>The results might be less relevant than a vector search, but a \"decent\" answer is always better than a \"timed out\" error.</p>\n<h3>Cached responses</h3>\n<p>Another strong strategy is <a href=\"https://ansezz.com/blog/redis-semantic-caching-rag/\">semantic caching with Redis</a>. If the circuit is open, check the cache for similar queries that were answered recently.</p>\n<p>Even if you can't generate a fresh answer, you might be able to serve a cached one. This keeps the user moving while your backend recovers.</p>\n<h3>LLM-only mode</h3>\n<p>If retrieval is what's failing, you can still send the user's prompt to the LLM with a note that external knowledge is currently unavailable. The LLM then answers from its general training data.</p>\n<p>It is a degraded experience, but it still works. Be open about it: tell the user that \"live\" data isn't available so they know to verify the response.</p>\n\n  <div>Every request waits on the slow vector DB until it times out. Workers pile up, memory climbs and the whole app stalls.</div>\n  <div>After the threshold, calls fail fast to keyword search, a cached answer or LLM-only mode. The app stays up while the DB recovers.</div>\n\n\nPick one fallback for your RAG endpoint today, even if it is only a keyword search over the same table, and write the message the user sees when it is active.\n\n<h2>Building it in Laravel</h2>\n<p>Since I spend a lot of time in the Laravel ecosystem, I lean on patterns that make this easy. You don't need to write the state machine from scratch.</p>\n<p>A community package like <code>ackintosh/ganesha</code> (a PHP circuit breaker), or a small custom wrapper around the <code>illuminate/http</code> client with a Redis-backed failure counter, gets the job done.</p>\n<h3>Wrap the client, catch the open state</h3>\n<p>The goal is to wrap your API calls in a block that understands these states. When you call your vector DB client, you wrap it in the breaker. If the call fails several times, the breaker trips.</p>\n<p>In the <code>catch</code> block, you handle the <code>CircuitBreakerOpenException</code> by returning your fallback data. This keeps your controllers clean and the failure handling in one place.</p>\n<h3>Keep health checks honest</h3>\n<p>You can also pair this with your <a href=\"https://ansezz.com/blog/coolify-docker-saas-hosting/\">SaaS hosting on Coolify</a> so your containers don't get killed by health checks just because an external API is slow. The breaker prevents the resource bloat that usually triggers those health-check failures.</p>\n<h2>Live telemetry and smart routing</h2>\n<p>Don't set a circuit breaker and walk away. Monitor it. You need live telemetry to see how often your circuits trip, and tools like Prometheus (or even simple logs piped to a dashboard) can tell you a lot.</p>\n<h3>Route around the failing region</h3>\n<p>If your primary vector DB in <code>us-east-1</code> keeps tripping but your secondary in <code>eu-west-1</code> is healthy, you can add smart routing. The breaker can act as a signal to your <a href=\"https://ansezz.com/blog/gpu-aware-load-balancing/\">load balancer</a> or internal router to shift traffic to the healthy region.</p>\n<p>This kind of <a href=\"https://ansezz.com/blog/event-driven-pubsub/\">event-driven architecture</a> makes your system self-healing. It doesn't wait for a human to wake up at 3am. It detects the failure, trips the breaker, uses the fallback and tries to recover on its own.</p>\n<figure>\n\nAdd an alert that fires when any circuit stays open for more than a few minutes. That usually means a real provider outage, and it is the one alert worth waking up for.\n\n<h2>Practical steps to get started</h2>\n<p>If you are ready to harden your AI infrastructure, start here:</p>\n<ol>\n<li><strong>Identify your weakest links.</strong> List every external call in your RAG pipeline. Usually it is the embedding API and the vector DB.</li>\n<li><strong>Define your thresholds.</strong> How many slow requests will you tolerate? Start with a 50% failure rate over 30 seconds and a 2-second timeout.</li>\n<li><strong>Choose your fallbacks.</strong> Decide what happens when the breaker is open: an error, a cache or keyword search.</li>\n<li><strong>Wrap your clients.</strong> Use a library to wrap your HTTP or database calls. Don't build the state machine yourself unless you have a very specific use case.</li>\n<li><strong>Monitor the trips.</strong> Alert when a circuit stays open for more than a few minutes. This usually means a major provider outage that needs your attention.</li>\n</ol>\n<p>The goal is to fail gracefully. Every system has issues, but the ones that survive don't let a small fire in a dependency burn down the whole house.</p>\n\n- Treat \"slow\" as a failure, not only `500` errors.\n- Closed, open, half-open: fail fast once the threshold is hit, then probe before you trust the service again.\n- Always have a fallback: keyword search, a cached answer or LLM-only mode with an honest notice.\n- Use a library such as `ackintosh/ganesha` instead of writing the state machine yourself.\n- Monitor trips and alert when a circuit stays open.\n\n<p>If your RAG stack stalls every time the vector database slows down, <a href=\"https://ansezz.com/services/\">here's how I help teams add circuit breakers and fallbacks to AI infrastructure</a>, or reach out through <a href=\"https://ansezz.com/contact/\">contact</a>.</p>\n<p>Have you ever had a slow dependency take down your entire application, or are you still relying on long timeouts and luck?</p>\n</figure></figure>",
      "date_published": "2026-05-23T00:00:00.000Z",
      "date_modified": "2026-05-23T00:00:00.000Z",
      "tags": [
        "architecture",
        "observability",
        "ai",
        "rag",
        "vector-search",
        "laravel",
        "infrastructure"
      ],
      "image": "https://ansezz.com/blog/circuit-breakers-vector-db/hero-626b5cfd.webp"
    },
    {
      "id": "https://ansezz.com/blog/message-queues-document-processing/",
      "url": "https://ansezz.com/blog/message-queues-document-processing/",
      "title": "Message queues for heavy-duty document processing",
      "summary": "Stop running embeddings in the request cycle. Build a document pipeline on message queues with staged workers, retries, dead-letter queues, and autoscaling.",
      "content_html": "<p>If your embeddings run inside the request cycle, the first 50MB PDF will take your app down. Put that work behind a queue.</p>\n\n- Document processing (OCR, extraction, chunking, embedding) is slow and unpredictable, so it belongs in background workers, not web requests.\n- Split the pipeline into queued stages with explicit retries, exponential backoff and a dead-letter queue for files that can never succeed.\n- Batch embedding calls, keep messages small and idempotent, and scale workers on queue depth instead of CPU.\n\n<p>If you run document embeddings inside your request-response cycle instead of behind a message queue, you are playing with fire.</p>\n<p>I have seen many developers build a beautiful RAG application that falls over the second a user uploads a 50MB PDF. The browser spins, the proxy timeout hits and the database locks up while your worker tries to chunk 500 pages of legal jargon in real time.</p>\n<h3>The heavy lifting problem</h3>\n<p>This is the classic \"heavy lifting\" problem in AI engineering. Document processing (OCR, text extraction, semantic chunking and embedding) is slow, unpredictable and resource-heavy.</p>\n<p>Forcing it into a synchronous web request gives you a bad user experience and a fragile system.</p>\n<h3>Decouple it</h3>\n<p>The fix is decoupling with message queues. Here is why async work belongs in a queue and how to build an ingestion pipeline that doesn't melt your server.</p>\n<p>If you want the broker-level foundations first, I cover them in <a href=\"https://ansezz.com/blog/scaling-with-rabbitmq/\">scaling with RabbitMQ</a>.</p>\n<h2>The synchronous trap</h2>\n<p>Imagine a user uploads a document to your SaaS. Your code receives the file, sends it to an extraction API and waits.</p>\n<p>Then it loops through the text to create chunks, sends each chunk to an embedding model and finally saves it all to pgvector.</p>\n<h3>Where it breaks</h3>\n<p>If the whole chain runs long, you hit a timeout somewhere: a proxy gateway timeout (Nginx defaults to 60s), a PHP <code>max_execution_time</code> or a platform request cap. The connection drops.</p>\n<p>If the embedding API has a momentary blip, the whole process fails and the user has to start over. While your server is busy with this heavy work, it isn't responding to other users.</p>\n<h3>The 100ms rule</h3>\n<p>A good rule of thumb: if it takes more than 100ms, consider making it async.</p>\n<p>Moving this work to a message queue gives your users immediate feedback (\"we're processing your file!\") while the heavy lifting happens safely in the background.</p>\n\n  <div>Upload, extract, chunk, embed and save in one request. A timeout or API blip throws away all the work, and other users wait.</div>\n  <div>Upload, save to S3, push a small message and return right away. Workers handle each stage, retry failures and report progress.</div>\n\n<h2>The anatomy of a document pipeline</h2>\n<p>A solid RAG pipeline is a series of decoupled stages, not one big function. I break it into modular steps, each triggered by a message in a queue, so you can scale each part on its own.</p>\n<h3>Four stages</h3>\n<p>Here is how I usually structure it:</p>\n<ol>\n<li><strong>Ingestion and discovery:</strong> a user uploads a file. You save it to S3 and push a small message to the queue with the <code>file_path</code> and <code>tenant_id</code>.</li>\n<li><strong>Parsing and normalization:</strong> a worker picks up the message, downloads the file and runs it through a parser like pdfplumber or an OCR service. It sends the raw text to the next queue.</li>\n<li><strong>Chunking:</strong> this worker splits the text into semantic sections. Keeping it in its own stage means you can swap chunking strategies (for example recursive character vs semantic) without re-running the heavy parsing step.</li>\n<li><strong>Embedding and indexing:</strong> the final stage batches the chunks, calls your embedding API (like OpenAI or a local model) and pushes the vectors into your vector DB.</li>\n</ol>\n<h3>Backpressure for free</h3>\n<p>This stage-based approach is what I discuss in my post on <a href=\"https://ansezz.com/blog/7-rag-mistakes-production/\">7 RAG mistakes to avoid in production</a>.</p>\n<p>It gives you backpressure control. If your vector DB slows down, the \"index\" queue grows, but the \"parsing\" workers keep going.</p>\n<p>When one stage slows down, its queue grows. Every other stage keeps working.</p>\n<figure>\n\nDraw your current document flow as boxes. Every arrow between two slow steps is a place to put a queue.\n\n<h2>Retries and dead letters</h2>\n<p>In the real world, things break. APIs time out. PDFs are malformed. Workers crash.</p>\n<h3>Opt in to retries</h3>\n<p>A message queue like Redis (with BullMQ or Laravel Queues) or SQS gives you retries with almost no code, but you do have to opt in:</p>\n<ul>\n<li><strong>BullMQ</strong> defaults to a single attempt, so set <code>attempts</code> and a <code>backoff</code> strategy explicitly.</li>\n<li><strong>Laravel queues</strong> take <code>--tries</code> and <code>backoff</code>.</li>\n<li><strong>SQS</strong> retries up to <code>maxReceiveCount</code>.</li>\n</ul>\n<p>When a worker fails, the message goes back onto the queue to be tried again after a delay. Use exponential backoff instead of hammering a failing API every 5 seconds: wait 10, then 60, then 300.</p>\n<h3>Quarantine what can't succeed</h3>\n<p>Some documents simply <em>cannot</em> be processed. Maybe it's a password-protected PDF or a corrupted file, and you don't want it retrying forever and clogging your workers.</p>\n<p>This is where a <strong>dead-letter queue (DLQ)</strong> comes in. After a set number of failed attempts, the message moves to the DLQ, a \"quarantine\" zone.</p>\n<p>SQS gives you this natively through the queue's redrive policy. With BullMQ or Laravel, you route exhausted jobs there yourself (listen for the <code>failed</code> event in BullMQ, or use <code>failed_jobs</code> plus a handler in Laravel). You can then inspect the failed jobs, fix the issue and re-queue them. My <a href=\"https://ansezz.com/tools/php-unserialize/\">PHP unserialize tool</a> decodes a <code>failed_jobs</code> payload so you can see exactly what the job received.</p>\n\nSet explicit attempts and an exponential backoff on your embedding job today, and add a failed-job handler that records the `doc_id` and error so you can replay it.\n\n<h2>Batching for efficiency</h2>\n<p>If you process 10,000 chunks, you don't want 10,000 separate API calls to your embedding provider. That is slow and expensive.</p>\n<p>Most embedding APIs and vector databases work much better with batches. A good worker pulls several messages from the queue (or gathers them in memory) and sends them as one bulk request.</p>\n<h3>Show progress</h3>\n<p>In Laravel, I often use job batching to track the progress of a large document. I can see exactly when 95% of a PDF is processed and update a progress bar for the user.</p>\n<p>For how this fits a larger architecture, see my thoughts on <a href=\"https://ansezz.com/blog/event-driven-pubsub/\">event-driven pub/sub systems</a>.</p>\n<h2>Event-driven prefetching</h2>\n<p>Queues help beyond ingestion. You can also use them for <strong>prefetching</strong>.</p>\n<p>If a user is chatting with an AI agent and the conversation is heading toward a topic, you can fire a background job to fetch related documents and warm the cache before they ask the next question. The AI feels very fast because the context is already \"ready\" when retrieval runs.</p>\n<h3>Keep the chat app simple</h3>\n<p>An event bus decouples the chat interface from these optimization tasks. The chat app just emits a <code>user_asked_question</code> event, and a background worker decides whether to prefetch more data or update the semantic cache.</p>\n<h2>Monitoring your message queue</h2>\n<p>Once you move to a queue-based system, request latency stops being the only number that matters. You need to watch your <strong>queue depth</strong>.</p>\n<p>If the queue grows faster than your workers can clear it, you have a bottleneck.</p>\n<h3>Add workers when it grows</h3>\n<p>Tools like Docker and Coolify make this easy. I can spin up five more worker containers to handle a sudden surge in uploads.</p>\n<p>You can read more about how I manage this infra in my <a href=\"https://ansezz.com/blog/coolify-docker-saas-hosting/\">Coolify and Docker guide</a>.</p>\n<figure>\n\nGraph queue depth per stage next to worker count. If depth keeps rising while CPU stays low, scale on depth instead of CPU.\n\n\n- **Never store large files in the queue.** Pass references (like an S3 key) and keep messages small.\n- **Make tasks idempotent.** Assume a message might be processed twice, and use `upsert` instead of `insert` in your vector DB.\n- **Use structured logging.** Every worker log should include the `doc_id` and `tenant_id`, or \"why did this file fail?\" has no answer.\n- **Scale on queue depth.** Add workers based on how many messages are waiting, not CPU alone.\n- **Separate worker pools.** Keep \"fast\" tasks (like metadata updates) apart from \"slow\" ones (like OCR and embedding), so a huge PDF never blocks a simple name change.\n\n<p>A document pipeline is about respecting the time it takes to process data. If big uploads are timing out your RAG app, <a href=\"https://ansezz.com/services/\">here's how I help teams build queue-based ingestion pipelines with retries and autoscaling</a>, or drop a note via <a href=\"https://ansezz.com/contact/\">contact</a>.</p>\n<p>How are you handling long-running AI tasks today: still fighting request timeouts, or already using a queue?</p>\n</figure></figure>",
      "date_published": "2026-05-22T00:00:00.000Z",
      "date_modified": "2026-05-22T00:00:00.000Z",
      "tags": [
        "architecture",
        "messaging",
        "ai",
        "rag",
        "redis",
        "laravel",
        "infrastructure"
      ],
      "image": "https://ansezz.com/blog/message-queues-document-processing/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/rate-limiting-ai-wallet/",
      "url": "https://ansezz.com/blog/rate-limiting-ai-wallet/",
      "title": "Rate limiting: protecting your AI wallet",
      "summary": "One runaway agent loop can mean a $5,000 LLM bill. Why request-per-second limits lie, and how hierarchical token-bucket limits protect your margins.",
      "content_html": "<p>One runaway agent loop is all it takes to wake up to a $5,000 LLM bill. In AI apps, rate limits protect your margins as much as your servers.</p>\n\n- Requests per second hide cost: one request can be 50 tokens, the next 128,000. Limit tokens, not pings.\n- Use three layers of token buckets in Redis: a global provider cap, a per-tenant budget tied to the plan, and a per-user share inside each tenant.\n- Degrade gracefully when a budget runs out, and keep your own caps even with provider hard limits, because a provider cap stops every tenant at once.\n\n<p>If you're building AI-powered SaaS or RAG systems, the main threat is often a \"denial of wallet\" attack rather than a server crash.</p>\n<p>A buggy client, a malicious user or your own experimental agent can spam your endpoints and burn through tokens and credits in minutes.</p>\n<h3>Tokens, not requests</h3>\n<p>Traditional web apps watch requests per second to keep the CPU from melting. With LLMs, you watch tokens per minute to keep the bank account from draining. For a quick feel of how text turns into tokens, try my <a href=\"https://ansezz.com/tools/token-counter/\">token counter</a>.</p>\n<p>You need limits that understand cost, context and the \"noisy neighbor\" problem before a single prompt reaches your vector DB.</p>\n<h2>Why requests per second lie</h2>\n<p>In a standard Laravel or Node app, a request is a request. Some take longer, but they use roughly similar resources.</p>\n<p>In AI engineering, one request might be a 50-token greeting and the next a 128,000-token context dump for a RAG pipeline.</p>\n<h3>The noisy neighbor bill</h3>\n<p>If you only limit requests, a user can stay inside \"10 requests per minute\" and still cost you 100x more than an average user. That's where the <a href=\"https://ansezz.com/blog/laravel-multi-tenancy/\">noisy neighbor problem</a> turns into a financial one.</p>\n<p>To protect margins, move from counting pings to counting cost.</p>\n<h2>Anatomy of a denial of wallet attack</h2>\n<p>A denial of wallet (DoW) attack is the AI version of a DDoS. The goal is to exhaust your quotas or budget until the service stops, or until you pay a huge bill.</p>\n<h3>Three ways it happens</h3>\n<ol>\n<li><strong>The agent loop:</strong> an autonomous agent gets stuck and calls your tools over and over with no \"max steps\" ceiling.</li>\n<li><strong>The scrapers:</strong> bots try to pull your whole knowledge base by querying every permutation of your RAG system.</li>\n<li><strong>The dev mistake:</strong> someone wires an LLM-powered autocomplete to a search bar that fires on every keystroke.</li>\n</ol>\n<p>Without token-aware limits, the provider eventually answers with <code>429</code> rate-limit errors. By then the money is already spent.</p>\n<figure>\n<h2>Hierarchical rate limiting</h2>\n<p>I use three layers of limits at the <a href=\"https://ansezz.com/blog/api-gateway-ai-stack/\">API gateway</a>. Even if one tenant goes rogue, the rest of the platform stays healthy.</p>\n<h3>1. The global provider layer</h3>\n<p>This is the last line of defense. If your provider quota is 500,000 tokens per minute (TPM), set your internal global limit to 450,000.</p>\n<p>The buffer lets you degrade on your own terms instead of hitting a wall of provider <code>429</code>s, which means failed user requests and retry timing you don't control.</p>\n<h3>2. The tenant layer</h3>\n<p>Every customer gets its own bucket, usually tied to the subscription tier. A \"Pro\" tenant might get 50,000 TPM while a \"Free\" one is capped at 2,000. No single company can eat your whole global quota.</p>\n<h3>3. The user or session layer</h3>\n<p>Inside a tenant you still need limits, so one employee can't use the whole organization's allocation. I set these at about 20% of the tenant's capacity.</p>\n<table>\n<thead>\n<tr>\n<th>Layer</th>\n<th>Protects against</th>\n<th>Example limit</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Global</strong></td>\n<td>Hitting the provider's own limits</td>\n<td>90% of provider TPM</td>\n</tr>\n<tr>\n<td><strong>Tenant</strong></td>\n<td>One customer draining the platform</td>\n<td>Pro 50,000 / Free 2,000 TPM</td>\n</tr>\n<tr>\n<td><strong>User</strong></td>\n<td>One person draining their company</td>\n<td>About 20% of tenant budget</td>\n</tr>\n</tbody>\n</table>\n<p>Count what a request costs, not how many requests arrive.</p>\n<h2>The token bucket algorithm</h2>\n<p>For most builds I use a <strong>token bucket</strong> backed by Redis. It refills at a steady rate and lets a request through only if enough tokens are available, which absorbs short bursts while capping the sustained rate. For how it differs from a leaky bucket, see <a href=\"https://ansezz.com/blog/rate-limiting-vs-throttling/\">rate limiting vs throttling</a>.</p>\n<p>Each tenant or user has a bucket. For every prompt, you estimate the total cost (input tokens plus the requested <code>max_tokens</code>). If the bucket has enough, the request proceeds and the tokens are deducted.</p>\n<h3>In Laravel middleware</h3>\n<p>On a Laravel stack, you can do this in middleware with Redis, where atomic operations keep per-tenant counters cheap and race-free.</p>\n<pre><code>// A simplified token-bucket check in Laravel middleware\npublic function handle($request, Closure $next)\n{\n    $tenantId = $request-&gt;user()-&gt;tenant_id;\n    $estimatedTokens = $this-&gt;tokenizer-&gt;estimate($request-&gt;input('prompt'));\n\n    if (!$this-&gt;limiter-&gt;consume(\"tenant:{$tenantId}:tokens\", $estimatedTokens)) {\n        return response()-&gt;json(['error' =&gt; 'token budget exceeded'], 429);\n    }\n\n    return $next($request);\n}\n</code></pre>\n\nPull last week's LLM usage and group it by tenant and by user. If the top 1% of users account for more than a third of your tokens, add the tenant and user layers before your next launch.\n\n<h2>Adaptive throttling</h2>\n<p>What happens when a user hits the limit? The default is a <code>429</code>. I prefer graceful degradation, which I call <strong>adaptive throttling</strong>.</p>\n<h3>Softer than a hard no</h3>\n<ul>\n<li><strong>Degrade the model:</strong> route the request to a smaller, cheaper model in the same family.</li>\n<li><strong>Trim the context:</strong> drop some of the retrieved RAG documents to cut input tokens.</li>\n<li><strong>Queue the request:</strong> for non-interactive work like background summarization, push it to a message queue and process it when the bucket refills.</li>\n</ul>\n<p>The user experience stays intact while your margins stay protected.</p>\n\n  <div>The tenant hits the budget and every request returns `429` until the bucket refills. Users see errors in the middle of their work.</div>\n  <div>Interactive requests switch to a cheaper model with less context, and background jobs wait in a queue. Users get slower or shorter answers, not failures.</div>\n\n<h2>Limiting the hidden RAG calls</h2>\n<p>In a RAG system, one user query often triggers several backend actions:</p>\n<ol>\n<li>One embedding call for the query.</li>\n<li>One search against the vector database.</li>\n<li>One or more LLM calls for the final answer.</li>\n</ol>\n<p>If you only limit the final LLM call, search queries can still hammer your vector database. Treat the whole RAG flow as one unit of work with its own combined budget.</p>\n<p>There are plenty of <a href=\"https://ansezz.com/blog/7-rag-mistakes-production/\">common RAG production mistakes</a>, and limiting only the LLM call is one of the most overlooked.</p>\n<figure>\n<h2>Steps to take this week</h2>\n<h3>1. Set provider spend controls, then cap in your app</h3>\n<p>Both major providers now offer hard spend limits. OpenAI's organization and project hard limits return a <code>429</code> (<code>organization_spend_limit_exceeded</code> or <code>project_spend_limit_exceeded</code>) once tracked spend reaches the amount. Enforcement isn't instantaneous, so spend can slightly overshoot, and plain spend alerts only notify.</p>\n<p>On Anthropic, a spend limit you set in the Console makes requests fail with a <code>400</code> until you raise it or the period resets.</p>\n<p>Turn them on as a backstop. A provider cap stops every tenant at once, so the per-tenant caps still belong in your app.</p>\n<h3>2. Enforce <code>max_tokens</code></h3>\n<p>Never allow an uncapped response. Set a sane default for <code>max_tokens</code> on every API call.</p>\n<h3>3. Set a per-request timeout</h3>\n<p>If an LLM call runs longer than 30 seconds, kill it. Slow calls are often the first sign of a system about to spiral.</p>\n\nOpen your provider console today and set a hard spend limit at about twice your normal monthly spend, plus alerts at 50% and 80%. Then write down what your app should do when that limit returns errors.\n\n\n- **Limit tokens, not requests.** Request counts hide the 100x cost spread between prompts.\n- **Layer your buckets.** Global, tenant and user limits keep one bad actor from hurting everyone.\n- **Degrade before you deny.** Cheaper models, trimmed context and queues beat a wall of `429`s.\n- **Budget the whole RAG flow.** Embeddings and vector searches cost money too.\n- **Use provider hard limits as a backstop.** Your own caps do the fine-grained work.\n\n<p>Rate limiting is part of your business model in the AI era. You can't scale a product that lets one user run up a thousand-dollar bill in the first hour. If you're adding AI to a multi-tenant product, <a href=\"https://ansezz.com/services/\">here's how I help teams build token budgets and gateway limits</a>, or <a href=\"https://ansezz.com/contact/\">drop me a note</a>.</p>\n<p>Have you seen a \"denial of wallet\" happen in the wild, or are you still relying on provider limits alone?</p>\n</figure></figure>",
      "date_published": "2026-05-21T00:00:00.000Z",
      "date_modified": "2026-05-21T00:00:00.000Z",
      "tags": [
        "architecture",
        "laravel",
        "security",
        "multi-tenancy",
        "redis",
        "api-design",
        "llm",
        "rag"
      ],
      "image": "https://ansezz.com/blog/rate-limiting-ai-wallet/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/api-gateway-ai-stack/",
      "url": "https://ansezz.com/blog/api-gateway-ai-stack/",
      "title": "API gateway: the front door of your AI stack",
      "summary": "Stop exposing LLM providers to your frontend. The API gateway pattern for AI apps: tenant isolation, model aliases, rate limiting, and streaming-safe timeouts.",
      "content_html": "<p>Stop exposing your models to the wild. Put a gateway between your users and every LLM call.</p>\n\n- Calling a RAG orchestrator or an LLM provider straight from the frontend is slow, insecure and the fastest route to a surprise five-figure bill.\n- An API gateway centralizes auth, tenant isolation, model routing and per-user rate limits before any token is spent.\n- AI traffic needs two special settings: no response buffering for streaming, and upstream timeouts long enough for slow generations.\n\n<p>If you are building a production AI app, sending requests directly from your frontend to a RAG orchestrator (or, god forbid, straight to an LLM provider) is a liability.</p>\n<p>It is slow. It is insecure. And it is the fastest way to wake up to a five-figure bill you didn't plan for.</p>\n<p>I have spent over a decade building software. Engineering for \"it works\" is not the same as engineering for \"it scales.\"</p>\n<p>In AI, scale means cost, latency and data safety as much as traffic.</p>\n<h2>What can go wrong without a gateway</h2>\n<p>Imagine a \"denial of wallet\" attack where a malicious script spams your completions endpoint. Without a gatekeeper, your API keys are sitting ducks.</p>\n<p>Or worse, imagine a multi-tenant app where one user's prompt accidentally retrieves another user's private data from your vector DB.</p>\n<p>This is where the API gateway comes in. It is the first line of defense and the brain of your infrastructure. It handles the boring but critical work so your RAG logic can stay focused on being smart.</p>\n<h2>The gatekeeper pattern</h2>\n<p>At its core, an API gateway is a reverse proxy that sits between your users and your backend services.</p>\n<p>For an AI stack, forwarding traffic is the smallest part of its job. It is the central point for auth, routing and rate limiting. (If the distinction blurs for you, I break it down in <a href=\"https://ansezz.com/blog/load-balancer-vs-api-gateway/\">load balancer vs API gateway</a>.)</p>\n<p>When a request hits your gateway, it runs a gauntlet of checks before it ever touches a model. This \"gatekeeper\" makes sure every millisecond of GPU time and every cent of token cost is intentional.</p>\n<p>Every token you pay for should pass a checkpoint first.</p>\n<h2>Authentication and tenant isolation</h2>\n<p>In a typical SaaS, authentication is about knowing who the user is. In an AI-powered SaaS, it is about data sovereignty.</p>\n<p>If you are building a RAG system, your biggest risk is cross-tenant data leakage. To avoid <a href=\"https://ansezz.com/blog/7-rag-mistakes-production/\">common RAG mistakes</a>, handle identity at the very edge.</p>\n<h3>JWT claims to tenant header</h3>\n<p>I prefer JWTs (JSON Web Tokens) with custom claims. When a request hits the gateway, I validate the token and extract the <code>tenant_id</code>.</p>\n<p>That ID is injected into the request headers before it is passed to the RAG orchestrator.</p>\n<h3>The orchestrator never guesses</h3>\n<p>The orchestrator receives a verified <code>x-tenant-id</code> header and uses it to apply metadata filters on the vector database. The user only \"sees\" data they are allowed to see.</p>\n<p>No tenant ID? No query. Period.</p>\n\nSend a request to your RAG endpoint with no tenant header and one with a forged tenant ID. Both should be rejected at the gateway before the orchestrator logs anything.\n\n<figure>\n<h2>Smart routing for model flexibility</h2>\n<p>The AI world moves fast. Today you are using GPT-6 Astra. Tomorrow, Claude Opus 5.5 might be the better play.</p>\n<p>Next week, you might want to test a fine-tuned open-weights model, like Meta's Muse Glimmer, running on your own infrastructure via <a href=\"https://ansezz.com/blog/coolify-docker-saas-hosting/\">Docker and Coolify</a>.</p>\n<p>If your model logic is hardcoded into your frontend or a single monolithic backend, switching models is a nightmare. An API gateway solves this with smart routing.</p>\n<h3>Model aliases</h3>\n<p>I use the gateway to create \"model aliases.\" Instead of calling a specific model, the frontend calls a generic endpoint like <code>/chat/pro</code>. The gateway then decides where to send the request based on:</p>\n<ol>\n<li><strong>User tier.</strong> Free users get routed to a cheaper, faster model like GPT-6 Luna or Claude Haiku. Pro users get the heavy hitters.</li>\n<li><strong>Versioning.</strong> Run an A/B test by routing 10% of traffic to a new model version without changing a single line of client-side code.</li>\n<li><strong>Failover.</strong> If OpenAI is having an outage, the gateway can reroute traffic to an Anthropic backup.</li>\n</ol>\n<p>This level of abstraction is what separates a weekend project from a resilient SaaS product.</p>\n\n  <div>The frontend calls one provider's model by name. A model swap, an A/B test or an outage means a client release.</div>\n  <div>The frontend calls `/chat/pro`. The gateway picks the model by tier, test bucket or provider health, and you change it in config.</div>\n\n<h2>Rate limiting: protecting the wallet</h2>\n<p>We used to rate limit to protect our CPUs. Now we rate limit to protect our bank accounts.</p>\n<p>AI requests are asymmetric. A user sends a 50-word prompt, and the model might generate a 1,000-word response. The cost difference is massive.</p>\n<h3>Global and per-tenant limits</h3>\n<p>A good gateway setup allows tiered rate limiting. Set global limits so the whole system can't be overwhelmed, and set per-tenant or per-user limits too.</p>\n<p>I usually implement this with Redis. The gateway checks the user's quota in real time.</p>\n<p>If they have exceeded their daily token limit or their requests-per-minute (RPM) cap, the gateway returns <code>429 Too Many Requests</code> immediately. I go deeper on the token-budget angle in <a href=\"https://ansezz.com/blog/rate-limiting-ai-wallet/\">rate limiting to protect your AI wallet</a>.</p>\n<h3>Why it pays off</h3>\n<p>This saves your backend from doing expensive work you won't get paid for.</p>\n<p>It also stops \"noisy neighbors\": one user scripting an automated tool that hogs your capacity and makes the app slow for everyone else.</p>\n\nPick your most expensive AI route and set two limits on it today: a per-user daily token cap and a global requests-per-minute ceiling. Return a clear 429 message so the frontend can explain it.\n\n<h2>Handling the AI-specific quirks</h2>\n<p>Gateways for AI need to handle two things differently than traditional web apps: streaming and long-running requests.</p>\n<h3>Streaming support</h3>\n<p>Most modern AI apps use Server-Sent Events (SSE) to stream responses word by word. Some older gateways or load balancers \"buffer\" the entire response before sending it to the client. This kills the user experience.</p>\n<p>Make sure your gateway (whether you use <a href=\"https://konghq.com/\">Kong</a>, <a href=\"https://tyk.io/\">Tyk</a> or a custom <a href=\"https://ansezz.com/blog/laravel-multi-tenancy/\">Laravel solution</a>) disables buffering for AI routes.</p>\n<p>The data should flow through the gateway like water through a pipe, not like a bucket that needs to fill first.</p>\n<h3>Extended timeouts</h3>\n<p>Traditional APIs expect a response in 1 to 2 seconds. A complex RAG query with multiple vector searches and a large model generation might take 30 seconds or more.</p>\n<p>You need to adjust your gateway's \"upstream timeout\" settings:</p>\n<table>\n<thead>\n<tr>\n<th>Gateway</th>\n<th>Default upstream timeout</th>\n<th>Can you raise it?</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Kong</td>\n<td>60 seconds</td>\n<td>Yes, per service</td>\n</tr>\n<tr>\n<td>AWS API Gateway (REST)</td>\n<td>29 seconds</td>\n<td>Only on Regional or private REST APIs, via a service quota increase that may cost throttle headroom</td>\n</tr>\n</tbody>\n</table>\n<p>Leave those defaults in place and your users will see <code>504 Gateway Timeout</code> errors even when your models are working perfectly.</p>\n\nTime your slowest real RAG query end to end, add a safety margin, and set that as the upstream timeout on your AI routes only. Keep the short default everywhere else.\n\n<figure>\n<h2>Practical steps for your stack</h2>\n<p>You don't need a massive team to set this up. Here is how I usually approach it depending on the project size.</p>\n<h3>For startups</h3>\n<p>Use a cloud-native gateway like AWS API Gateway or Azure API Management. They are serverless, scale automatically and integrate directly with Cognito or Entra ID for auth.</p>\n<h3>For self-hosters</h3>\n<p>Kong is the gold standard, with a great ecosystem of plugins for rate limiting and auth. If you are comfortable with PHP, a thin Laravel app acting as a gateway works surprisingly well for custom logic.</p>\n<h3>For Shopify devs</h3>\n<p>If you are building <a href=\"https://ansezz.com/blog/agentic-commerce-shopify/\">agentic commerce tools</a>, use the gateway to handle Shopify HMAC validation before passing the request to your AI agents. When a signature fails, my <a href=\"https://ansezz.com/tools/shopify-hmac-verifier/\">Shopify webhook HMAC verifier</a> helps you find the cause.</p>\n<h2>Why the gateway is a design choice</h2>\n<p>The API gateway is infrastructure, but it is also a statement: your AI logic is too valuable (and too expensive) to leave unprotected.</p>\n<p>By centralizing auth, routing and rate limiting, you make your system more modular. You can swap models, change pricing tiers and update security policies without touching the core code that makes your AI \"smart.\"</p>\n\n- **Centralize auth.** Never let your RAG orchestrator handle raw user authentication. Do it at the gateway.\n- **Inject tenant context.** Verify the user at the gateway and inject a `tenant_id` header to enforce data isolation.\n- **Set global and per-user limits.** Protect your wallet from both malicious attacks and accidental bugs.\n- **Configure for streaming.** Make sure your gateway doesn't buffer responses, or your \"typing\" effect will break.\n- **Use model aliases.** Route to `/chat/pro` instead of a specific model name to keep your stack flexible.\n\n<p>If you're putting an LLM-backed product in front of real users and want auth, routing and cost limits done right, <a href=\"https://ansezz.com/services/\">here's how I help teams build the gateway layer</a>.</p>\n<p>Are you still letting your frontend talk directly to your LLM providers? If so, what is the one thing stopping you from putting a gateway in front of it?</p>\n</figure></figure>",
      "date_published": "2026-05-20T00:00:00.000Z",
      "date_modified": "2026-05-20T00:00:00.000Z",
      "tags": [
        "architecture",
        "api-design",
        "ai",
        "rag",
        "security",
        "multi-tenancy",
        "messaging",
        "infrastructure"
      ],
      "image": "https://ansezz.com/blog/api-gateway-ai-stack/hero-d3f4d0f7.webp"
    },
    {
      "id": "https://ansezz.com/blog/shopify-storefront-web-components/",
      "url": "https://ansezz.com/blog/shopify-storefront-web-components/",
      "title": "Shopify Storefront Web Components: headless light",
      "summary": "Headless used to mean six engineers and a Hydrogen rebuild. Shopify Storefront Web Components drop products, collections, and cart into any page with a script.",
      "content_html": "<p>Headless used to mean a team of engineers just to show a \"Buy\" button. Shopify Storefront Web Components put products, collections and a cart on any page with one script tag.</p>\n\n- Storefront Web Components are standard custom elements that talk to the Storefront API for you: no React, no build step, just HTML.\n- You load one module script, set the store with ``, pick a product with `` and render fields with `` and ``.\n- Use them for buy buttons, landing pages and content sites; reach for Hydrogen when you need a full custom storefront with server-side logic.\n\n<p>If you wanted a custom Shopify experience outside Liquid themes, you used to face a big architectural decision.</p>\n<p>You built a full React or Hydrogen app, managed server-side rendering, handled routing and hoped your SEO survived. For many brands that was like buying a semi-truck when they needed a bicycle.</p>\n<h3>The gatekeeper</h3>\n<p>Traditional headless kept simple content sites on Astro or WordPress from selling products without a jarring jump to a subdomain. We've spent years over-engineering problems a few custom elements could solve.</p>\n<p>Storefront Web Components change the math. Adding commerce to a site starts with a script tag.</p>\n<h2>The headless headache</h2>\n<p>Building a headless store feels like building a house from scratch. You handle the foundation (hosting), the plumbing (GraphQL queries) and the wiring (state management).</p>\n<h3>Weeks for a working cart</h3>\n<p>You can spend weeks configuring Hydrogen or a custom Next.js setup just to get a cart that works. Once it's live, you maintain the Node.js environment and the middleware too.</p>\n<p>It's expensive and slow to deploy, and for many use cases it's overkill. We need the flexibility of headless without the upkeep of a heavy framework.</p>\n<figure>\n<h2>Custom elements, no framework</h2>\n<p>Storefront Web Components are framework-agnostic. They're standard Web Components (custom elements) that handle the calls to Shopify's Storefront API.</p>\n<p>All you need is HTML: no React, no build step.</p>\n<h3>Headless light</h3>\n<p>This approach embeds products, collections and a working cart (<code>&lt;shopify-cart&gt;</code>) in your existing stack. On a static site generator or an older CMS, they act as a bridge: your content stays where it is and the commerce loads in on the page.</p>\n<h2>Getting started in 5 minutes</h2>\n<p>Here's the workflow I use when I want to move fast.</p>\n<h3>1. Connect your store</h3>\n<p>Add the module script and a <code>&lt;shopify-store&gt;</code> element to your HTML. That connects the page to your Shopify domain.</p>\n<pre><code>&lt;!-- Load the components script. Docs:\n     https://shopify.dev/docs/api/storefront-web-components --&gt;\n&lt;script type=\"module\" src=\"https://cdn.shopify.com/storefront/web-components.js\"&gt;&lt;/script&gt;\n\n&lt;shopify-store store-domain=\"https://your-store.myshopify.com\"&gt;&lt;/shopify-store&gt;\n</code></pre>\n<p>The <code>public-access-token</code> attribute is optional. Add one from the Headless channel in your Shopify admin when you need inventory, metafields or metaobjects. Titles and prices work without it.</p>\n<h3>2. Define the context</h3>\n<p><code>&lt;shopify-context&gt;</code> tells the page which product or collection to load. Pass the handle from your Shopify admin.</p>\n<pre><code>&lt;shopify-context type=\"product\" handle=\"awesome-tshirt\"&gt;\n  &lt;template&gt;\n    &lt;!-- your content goes here --&gt;\n  &lt;/template&gt;\n&lt;/shopify-context&gt;\n</code></pre>\n<h3>3. Display the data</h3>\n<p>Inside the template, <code>&lt;shopify-data&gt;</code> pulls specific fields with dot notation, which feels familiar if you've worked with Liquid or JavaScript objects.</p>\n<pre><code>&lt;shopify-data query=\"product.title\"&gt;&lt;/shopify-data&gt;\n&lt;shopify-money\n  query=\"product.selectedOrFirstAvailableVariant.price\"\n&gt;&lt;/shopify-money&gt;\n</code></pre>\n\n  <div>A React app, a Node server, GraphQL queries, cart state and a deploy pipeline, all to show a product card on your marketing site.</div>\n  <div>One module script, a store tag and a product context block with the fields you want, pasted into the page you already have.</div>\n\n\nPick one product page on your content site and replace its \"Shop now\" link with a `` block that shows the title, price and an add-to-cart button. Compare clicks to checkout over the next two weeks.\n\n<h2>llms.txt for AI-assisted builds</h2>\n<p>Shopify publishes a machine-readable <a href=\"https://webcomponents.shopify.dev/llms.txt\">llms.txt</a> for Storefront Web Components. To write one for your own store or docs, my <a href=\"https://ansezz.com/tools/llms-txt-generator/\">llms.txt generator</a> builds the file from a short form and checks it against the format.</p>\n<p>If you've followed <a href=\"https://ansezz.com/blog/agentic-workflows-vibe-coding/\">agentic workflows and vibe coding</a>, you know context is everything for an AI model. Point your assistant (Claude, ChatGPT or similar) at this file and it learns the components' actual attributes and patterns.</p>\n<h3>Less guessing</h3>\n<p>Instead of guessing how a Shopify component should look, the model works from the official reference. With this link in my prompt, I can draft product landing pages much faster, and far fewer of them need fixing before they work.</p>\n<p>For more on feeding docs to agents, see how <a href=\"https://ansezz.com/blog/mcp-context-aware-agents/\">MCP context-aware agents</a> handle technical documentation.</p>\n<p>Give the model the official reference and it stops inventing attributes.</p>\n<h2>Web Components vs Hydrogen</h2>\n<p>Here's my rule of thumb.</p>\n<table>\n<thead>\n<tr>\n<th>Use Storefront Web Components when...</th>\n<th>Use Hydrogen when...</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>You want buy buttons or a mini-cart on an existing site</td>\n<td>You're building a full custom storefront</td>\n</tr>\n<tr>\n<td>You're shipping a landing page and speed matters most</td>\n<td>You need server-side logic and deep API integrations</td>\n</tr>\n<tr>\n<td>Your team won't maintain React and Node.js</td>\n<td>You want streaming, edge rendering and fine performance control</td>\n</tr>\n<tr>\n<td>You want to stay framework-agnostic</td>\n<td>You have the team to run it as a product</td>\n</tr>\n</tbody>\n</table>\n<p>For most teams starting out or modernizing a site, Web Components are the practical choice. They balance <a href=\"https://ansezz.com/blog/ai-vs-traditional-development/\">AI-driven development</a> with production stability. If you're still weighing the bigger call, see <a href=\"https://ansezz.com/blog/shopify-liquid-vs-headless/\">Shopify Liquid vs. headless</a>.</p>\n<figure>\n\nPaste the llms.txt URL into your AI assistant and ask it for a collection grid with 12 products and a cart. Check every tag and attribute it uses against the docs before you ship it.\n\n\n- **Web Components are \"headless light\":** commerce on any page without a framework.\n- **They work anywhere:** WordPress, Astro, plain HTML, even a local static file.\n- **Use the official `llms.txt`** so your AI assistant writes code against the real components.\n- **Avoid over-engineering:** if you don't need a large React app, don't build one.\n\n<p>Building for the web in 2026 is about removing friction, and these components remove most of it from headless. If you want commerce on a site that isn't Shopify, <a href=\"https://ansezz.com/services/\">here's how I help brands add Shopify to Astro, WordPress and custom sites</a>, or <a href=\"https://ansezz.com/contact/\">drop me a note</a>.</p>\n<p>Are you still building full React apps for simple stores, or have you tried custom elements yet?</p>\n</figure></figure>",
      "date_published": "2026-05-19T00:00:00.000Z",
      "date_modified": "2026-05-19T00:00:00.000Z",
      "tags": [
        "shopify",
        "shopify",
        "hydrogen",
        "agentic-commerce",
        "agentic-ai"
      ],
      "image": "https://ansezz.com/blog/shopify-storefront-web-components/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/agentic-commerce-shopify/",
      "url": "https://ansezz.com/blog/agentic-commerce-shopify/",
      "title": "How agentic commerce changes building on Shopify",
      "summary": "Agentic commerce is reshaping how you build Shopify. The shift from human-centric themes to agent-ready infrastructure: Catalog, UCP, and MCP.",
      "content_html": "<p>Your beautiful Shopify theme is about to become invisible. The next shopper has no eyes, never clicks a button and only reads your data.</p>\n\n- AI agents now do the discovery, comparison and checkout for a growing share of buyers, so a store that agents can't read drops out of the transaction.\n- Shopify's answer is three layers: the Universal Commerce Protocol (UCP), the Catalog API and Agentic Storefronts, with MCP as the tool binding underneath.\n- The developer job moves from pixels to protocols: native variants, rich Metaobjects, GraphQL, MCP servers and clean product data.\n\n<p>For a decade, we have obsessed over conversion rate optimization, pixel-perfect layouts and fast-loading Liquid templates. We built for human eyes.</p>\n<p>But a new type of shopper is arriving at the digital storefront. They don't have eyes, they don't click buttons, and they don't care about your hero slider.</p>\n<p>They are autonomous AI agents.</p>\n<p>The shift from human browsing to agentic commerce is a big architectural shift for Shopify. If you are still \"the theme guy\" or \"the Liquid expert,\" your skill set is about to hit a wall.</p>\n<p>I have been building on the web for over ten years, and the era of building for agents is here. It is time to move from designing pixels to designing protocols.</p>\n<h2>The end of the traditional funnel</h2>\n<p>Most developers still think the old way. We build a site, drive traffic to a landing page and hope the user finds the checkout. This is a manual, high-friction process.</p>\n<p>Today, buyers increasingly use tools like ChatGPT, Gemini or Microsoft Copilot to do the \"work\" of shopping. They ask for a waterproof hiking boot under $150 with specific shipping requirements.</p>\n<p>The AI doesn't visit your store to browse. It queries data.</p>\n<p>If your store isn't built to be \"read\" by an agent, you simply don't exist in that transaction.</p>\n\nAsk ChatGPT, Gemini and Copilot for one of your best-selling products the way a real buyer would phrase it. Note whether your store shows up, and whether the price and stock it quotes are correct.\n\n<h2>Why your current apps aren't enough</h2>\n<p>We have spent years building \"utility\" apps that handle simple automations. But Shopify is moving fast to pull those features into the core.</p>\n<p>With tools like Shopify Magic and Sidekick, the basic stuff (copywriting, image editing, simple workflows) is being commoditized.</p>\n<p>The real value is moving deeper into the stack. Agents need structured data, real-time inventory logic and a way to run a checkout without a browser window. A product description alone won't do.</p>\n<h3>Messy data breaks agent reasoning</h3>\n<p>If you rely on \"fake\" variants or messy metafields, you break the agent's ability to reason about your store.</p>\n<p>When an agent fails to understand your product structure, the buyer gets \"no results found\" or a hallucinated alternative. That is a lost sale for your client and a failed project for you.</p>\n<h2>Welcome to agentic commerce</h2>\n<p>Agentic commerce is a system where AI agents take over the discovery, comparison and execution phases of shopping. Shopify is laying the groundwork with three pillars.</p>\n<h3>1. Universal Commerce Protocol (UCP)</h3>\n<p>The open standard Shopify co-developed with Google for how agents discover, negotiate and transact with any merchant.</p>\n<p>It is composable: it defines the conversation, then plugs into MCP for tool access and AP2 for payment authorization. For a hands-on walkthrough, see my <a href=\"https://ansezz.com/blog/shopify-ucp-quick-start/\">Shopify UCP quick-start</a>.</p>\n<h3>2. Catalog API</h3>\n<p>UCP's discovery layer. It standardizes products into a universal taxonomy, verifies pricing and inventory in real time, and syndicates that structured data to connected AI surfaces.</p>\n<h3>3. Agentic Storefronts</h3>\n<p>The management layer. You configure product data once in the Shopify admin, then distribute and sell across AI channels (ChatGPT, Microsoft Copilot, Google's AI Mode, the Gemini app).</p>\n<p>As developers, our job is shifting from visual storefronts to \"agent-ready\" infrastructure.</p>\n\n  <div>Hero sliders, hover effects, split products for each color and specs hidden in a description blob. Success is a click-through rate.</div>\n  <div>Native variants, Metaobjects for specs and compatibility, clean GraphQL access and real-time stock. Success is an agent that quotes you correctly.</div>\n\n<figure>\n<h2>Why schema is the new CSS</h2>\n<p>In the agentic era, your structured data is your most important asset. A well-built JSON-LD schema or a clean set of Shopify Metaobjects is now worth more than a fancy hover effect.</p>\n<p>Agents rely on clarity. They need to know if a product works with another one, what the material breakdown is, and exactly when it will arrive.</p>\n<p>If you want to see how I've helped businesses modernize through better architecture, check out some of my <a href=\"https://ansezz.com/work/\">previous work</a>.</p>\n<h3>Where to focus</h3>\n<ul>\n<li><strong>Native variants only.</strong> Stop using \"split products\" or custom Liquid hacks to show variants. Agents need standard <code>ProductVariant</code> records to function.</li>\n<li><strong>Rich Metaobjects.</strong> Use them to build knowledge bases that agents can query: compatibility tables, brand story and detailed technical specs.</li>\n<li><strong>GraphQL mastery.</strong> The Shopify Admin and Storefront APIs are the primary languages of agents. If you aren't comfortable with complex GraphQL queries, you are already behind.</li>\n</ul>\n<p>An agent can't admire your layout. It can only read your data.</p>\n\nExport your catalog and count products that fake variants as separate products or hide specs in the description HTML. That count is the scope of your first \"agent-readiness\" cleanup.\n\n<h2>Building custom agents with MCP and Shopify</h2>\n<p>The <a href=\"https://ansezz.com/blog/mcp-context-aware-agents/\">Model Context Protocol (MCP)</a> lets us connect AI models to external data sources like the Shopify API. It is the same binding UCP uses to expose commerce tools to agents.</p>\n<p>Picture an \"inventory agent\" for a merchant. A plain alert fires when stock is low. This agent goes further: it queries recent sales trends via the Admin API, checks supplier lead times and suggests a restock amount, all through a chat interface.</p>\n<h3>Where Laravel and Docker fit</h3>\n<p>With Laravel and Docker, we can build custom middleware that acts as an MCP server and exposes specific Shopify \"tools\" to an AI agent.</p>\n<p>This deep technical work is what separates a senior engineer from a freelancer who only installs themes.</p>\n<h2>The Shopify developer's new roadmap</h2>\n<p>To thrive in the next five years, I recommend focusing on four technical areas.</p>\n<h3>1. Data hygiene as a service</h3>\n<p>Start offering \"agent-readiness\" audits. Clean up product data, normalize attributes and make sure all metadata is machine-readable. This is the new SEO. A short <a href=\"https://ansezz.com/tools/llms-txt-generator/\">llms.txt file</a> that points agents at your key pages is an easy first item on that list.</p>\n<h3>2. Headless and API-first builds</h3>\n<p>Liquid isn't dead, but headless architectures using Hydrogen and Oxygen fit the agentic future better. They force you to think in data and APIs rather than templates.</p>\n<p>If you're weighing the trade-offs, I broke them down in <a href=\"https://ansezz.com/blog/shopify-liquid-vs-headless/\">Shopify Liquid vs. headless</a>.</p>\n<h3>3. RAG and vector databases</h3>\n<p>Learn how to use RAG (retrieval-augmented generation) and tools like pgvector. You can build agents that search thousands of product reviews or support docs to give \"expert\" advice to shoppers.</p>\n<p>If you're new to this, start with <a href=\"https://ansezz.com/blog/picking-the-right-rag-stack/\">picking the right RAG stack</a>.</p>\n<h3>4. Agent trust tiers and the Agentic plan</h3>\n<p>Shopify classifies agent traffic into three trust tiers: Anonymous, Signed and Token. All three can search the catalog and build carts and checkouts. Only a Token-tier agent whose token has been granted purchase permission can call <code>complete_checkout</code> and finish the order.</p>\n<p>Direct checkout also depends on the channel. Copilot, Google's AI Mode and the Gemini app offer native checkout powered by UCP (Google's is still rolling out to more brands). In ChatGPT, the buyer completes the purchase on the merchant's online store inside an in-app browser.</p>\n<p>There is also a dedicated Agentic plan for brands on any platform. They list products in Shopify Catalog and sell through agentic storefronts with no monthly fee, paying card rates (from 2.9% + 30¢ USD online) when a product sells.</p>\n<p>For the identity side, see <a href=\"https://ansezz.com/blog/ucp-agent-profiles-are-the-new-oauth-app/\">UCP agent profiles</a> and <a href=\"https://ansezz.com/blog/buyer-linked-tokens-agent-checkout/\">buyer-linked tokens for agent checkout</a>.</p>\n<figure>\n<h2>The numbers behind the shift</h2>\n<p>The change is happening faster than most of us realize. Shopify reports that in Q1 2026:</p>\n<table>\n<thead>\n<tr>\n<th>Signal</th>\n<th>Q1 2026, year over year</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>AI-driven traffic to its stores</td>\n<td>Grew roughly 8x</td>\n</tr>\n<tr>\n<td>Orders from AI-powered searches</td>\n<td>Climbed nearly 13x</td>\n</tr>\n<tr>\n<td>New buyers via AI channels</td>\n<td>Ordering at close to 2x the rate of other channels</td>\n</tr>\n</tbody>\n</table>\n\nPick one client store and write a one-page \"agent-readiness\" report: variant structure, Metaobject coverage, stock accuracy and which AI channels are switched on in Agentic Storefronts. That page is the start of a new service line.\n\n\n- Move your product logic into native Shopify structures.\n- Double down on GraphQL and API integration.\n- Start experimenting with MCP servers and local LLMs for internal store operations.\n- Learn the trust tiers: only an approved Token-tier agent can complete checkout.\n- Remember that the agent is your new \"user.\" Optimize for its understanding first.\n\n<p>More and more of commerce will run through agents. It is a great time to be a developer if you are willing to learn the new protocols. You can read more <a href=\"https://ansezz.com/about/\">about me</a> and my background in web applications.</p>\n<p>If you want your Shopify catalog and checkout ready for AI agents, <a href=\"https://ansezz.com/services/\">here's how I help merchants and teams get agent-ready</a>, or drop a note via <a href=\"https://ansezz.com/contact/\">contact</a>.</p>\n<p>Are you building a store that only humans can see, or one that the whole world can buy from?</p>\n</figure></figure>",
      "date_published": "2026-05-18T00:00:00.000Z",
      "date_modified": "2026-05-18T00:00:00.000Z",
      "tags": [
        "shopify",
        "shopify",
        "agentic-commerce",
        "ai",
        "mcp",
        "hydrogen",
        "api-design"
      ],
      "image": "https://ansezz.com/blog/agentic-commerce-shopify/hero-d9958a30.webp"
    },
    {
      "id": "https://ansezz.com/blog/7-rag-mistakes-production/",
      "url": "https://ansezz.com/blog/7-rag-mistakes-production/",
      "title": "7 mistakes wrecking your production RAG stack",
      "summary": "Naive chunking, no reranker, embedding drift, latency blowups: the structural mistakes that wreck a production RAG stack, and the fixes that ship.",
      "content_html": "<p>A RAG demo takes an afternoon. A RAG system that users trust takes fixing the seven structural mistakes below.</p>\n\n- Most RAG failures in production come from retrieval, not from the model: bad chunks, no reranker, no hybrid search and no metadata filters.\n- Treat embeddings like a database schema. Pin the model, version the index and re-index when either changes.\n- Stop vibe-checking. A golden set of 50 to 100 questions scored on faithfulness, relevance and context precision tells you if a change helped.\n\n<p>Getting a RAG (retrieval-augmented generation) demo working is easy. You take a few PDFs, throw them into a vector database like Chroma or Pinecone, and ask a question. It feels like magic.</p>\n<p>If you're still choosing your foundation, my guide to <a href=\"https://ansezz.com/blog/picking-the-right-rag-stack/\">picking the right RAG stack</a> covers the trade-offs before you commit.</p>\n<p>But shipping RAG to production is where the magic dies.</p>\n<p>I've seen too many teams launch a feature only to find users getting irrelevant answers, waiting 10 seconds for a response, or getting \"I don't know\" for questions that are clearly in the documentation. When the \"vibe check\" fails at scale, your users lose trust.</p>\n<p>I've spent the last few years building <a href=\"https://ansezz.com/work/\">custom web applications</a> and AI systems, and I've had to fix these same leaks in my own stacks. Here is how to get from \"it works on my machine\" to a production-grade AI system.</p>\n<h2>1. Naive chunking is killing your context</h2>\n<p>Most people start with a simple character-based or token-based splitter. You tell the library to \"give me chunks of 500 tokens with a 50-token overlap.\"</p>\n<p>This is a mistake.</p>\n<p>Naive chunking treats your data like raw soup. It might cut a sentence in half, split a table in the middle of a row, or separate a code example from the explanation before it.</p>\n<p>If the retriever pulls only one of those halves, the LLM has zero chance of giving a correct answer.</p>\n<h3>The fix: semantic or structural chunking</h3>\n<p>I always recommend chunking on the actual structure of the document first. Use headers (H1, H2, H3), paragraphs or Markdown delimiters so related ideas stay together.</p>\n<p>For complex data, use recursive character splitting that respects newlines and punctuation before it falls back to raw token counts.</p>\n<p>You can compare strategies on your own text with my <a href=\"https://ansezz.com/tools/rag-chunk-splitter/\">RAG chunk splitter</a>. Bad chunking is one of the most common reasons <a href=\"https://ansezz.com/blog/why-your-rag-is-failing/\">your RAG fails in production</a>.</p>\n\nPaste one of your longest real documents into the [RAG chunk splitter](/tools/rag-chunk-splitter/) and count how many chunks cut a sentence or a table row in half. If it is more than a couple, switch to header-based splitting first.\n\n<h2>2. Skipping the reranker step</h2>\n<p>Vector search is great at finding \"roughly similar\" content, but it is not a precision instrument. It relies on cosine similarity, which documents can fool when they share a similar \"vibe\" but don't contain the answer.</p>\n<p>If you take the top 5 results from your vector store and shove them into the prompt, you're leaving quality on the table.</p>\n<h3>The fix: two-stage retrieval</h3>\n<p>I look at retrieval as a two-stage process:</p>\n<ol>\n<li><strong>Fast and broad.</strong> Pull the top 20 or 50 candidates from your vector database.</li>\n<li><strong>Slow and precise.</strong> Use a cross-encoder or a reranking model (like Cohere's Rerank or BGE-Reranker) to score those candidates against the query more accurately.</li>\n</ol>\n<p>The reranker acts like a bouncer at a club. It doesn't care if a document looks \"okay.\" It only lets in the ones that are relevant to the question.</p>\n<p>Vector search finds neighbors. A reranker finds answers.</p>\n<figure>\n<h2>3. Ignoring embedding drift and versioning</h2>\n<p>This is the silent killer. I've seen teams upgrade their embedding model from <code>text-embedding-ada-002</code> to <code>text-embedding-3-small</code> without re-indexing their entire database.</p>\n<p>Suddenly, the vectors generated for new queries don't \"line up\" with the vectors stored in the index, and similarity scores go haywire.</p>\n<p>Even worse is changing the preprocessing logic (like how you format chunks) while keeping the old vectors.</p>\n<h3>The fix: pin your models and version your index</h3>\n<p>Treat your embedding model like a database schema. If you change the model, you must re-index.</p>\n<p>I always include the model name and version in the metadata of every index I build. That way I can test a new model side by side without breaking the production flow.</p>\n<p>My <a href=\"https://ansezz.com/about/\">experience in cloud infrastructure</a> has taught me that consistency beats a \"better\" model that doesn't match its data.</p>\n\nAdd an `embedding_model` and `embedding_version` field to every chunk you write today, and make your query code refuse to search an index whose version does not match the model it is using.\n\n<h2>4. The \"lost in the middle\" latency problem</h2>\n<p>Everyone wants more context. We see context windows of 128k or even 1M tokens and think, \"great, I'll give the LLM everything!\" This is a trap for two reasons.</p>\n<h3>Latency</h3>\n<p>Feeding tens of thousands of tokens of context into an LLM inflates your time-to-first-token. It can push response times into double-digit seconds.</p>\n<h3>Lost in the middle</h3>\n<p>Models still struggle with the <a href=\"https://arxiv.org/abs/2307.03172\">\"lost in the middle\"</a> effect. They reliably use information at the start and end of a long context but tend to ignore facts buried in the center.</p>\n<p>A bigger <a href=\"https://ansezz.com/blog/context-window-vs-memory/\">context window is not the same as memory</a>, and stuffing it rarely helps.</p>\n<h3>The fix: set a latency budget</h3>\n<p>I start with a latency budget. If the user expects a response in under 2 seconds, I can't afford to send 20 chunks.</p>\n<p>I limit retrieval to the top 3 to 5 high-quality chunks and start streaming as soon as the first token is ready.</p>\n<p>If you need more data, use a multi-step approach: a cheaper model summarizes the retrieved chunks before the refined info goes to your main model.</p>\n<h2>5. Forgetting about hybrid search</h2>\n<p>Vector search is bad at finding specific keywords or unique identifiers.</p>\n<p>If a user asks for \"error code XF-904,\" a vector search might return documents about general error handling because the \"vibe\" is similar. It might miss the one document that actually mentions \"XF-904.\"</p>\n<h3>The fix: dense plus sparse</h3>\n<p>I always combine dense vector search with traditional sparse search (like BM25). Blend the two result lists with something like Reciprocal Rank Fusion (RRF).</p>\n<p>You get the semantic understanding of vectors and the keyword precision of full-text search. This is non-negotiable for enterprise search or technical documentation.</p>\n<p>If keyword precision matters more than you expect, it's worth weighing <a href=\"https://ansezz.com/blog/vector-search-vs-graph-search/\">vector search against graph search</a> for your retrieval strategy.</p>\n\n  <div>Fixed 500-token chunks, top 5 from vector search straight into the prompt, one embedding model nobody tracks, and a few manual test questions.</div>\n  <div>Structural chunks, metadata filters first, hybrid search, a reranker over 20 to 50 candidates, a versioned index and a scored golden set.</div>\n\n<h2>6. Failing to filter by metadata</h2>\n<p>If your RAG system holds documents for different customers, versions or dates, pure vector search will betray you.</p>\n<p>You might ask about \"API changes in 2024\" and get results from 2022 because they share similar keywords.</p>\n<p>Relying on the LLM to \"ignore\" the wrong dates in the context wastes tokens and invites hallucinations.</p>\n<h3>The fix: hard metadata filters</h3>\n<p>Apply filters before the vector search even happens. If you know the user wants \"v2\" of your documentation, filter the query to chunks with <code>version: '2'</code>.</p>\n<p>This shrinks the search space and improves accuracy. I use this heavily when building <a href=\"https://ansezz.com/work/\">Shopify apps</a>, where data must be strictly siloed by shop ID.</p>\n\nList the three questions your users ask most and write down which metadata field (version, date, tenant, product) would narrow each one. Add those fields to your index before you tune anything else.\n\n<h2>7. Vibe-based evaluation</h2>\n<p>How do you know your RAG stack is getting better? Most devs ask a few questions, see that the answers look okay, and ship.</p>\n<p>This \"vibe-checking\" doesn't work. When you change a prompt or a chunk size, you might improve one answer while breaking ten others you didn't check.</p>\n<h3>The fix: a golden evaluation set</h3>\n<p>I use the Ragas framework or simple LLM-as-a-judge patterns to run automated evals. I keep a \"golden set\" of 50 to 100 questions with ground-truth answers.</p>\n<p>Every time I change the architecture, I run the eval and look at three metrics:</p>\n<table>\n<thead>\n<tr>\n<th>Metric</th>\n<th>Question it answers</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Faithfulness</strong></td>\n<td>Is the answer actually derived from the context?</td>\n</tr>\n<tr>\n<td><strong>Answer relevance</strong></td>\n<td>Does it answer the user's question?</td>\n</tr>\n<tr>\n<td><strong>Context precision</strong></td>\n<td>Are the retrieved chunks actually useful?</td>\n</tr>\n</tbody>\n</table>\n<figure>\n<h2>Practical rules for your stack</h2>\n<ul>\n<li><strong>Start with metadata.</strong> Don't let the vector database guess. If you have categories or dates, use them as hard filters.</li>\n<li><strong>Rerank by default.</strong> It's the single biggest quality jump you can make for the lowest effort.</li>\n<li><strong>Monitor retrieval, not only generation.</strong> If your retriever fails, the best LLM in the world can't save you. Log your top-k retrieval results separately.</li>\n<li><strong>Don't over-engineer.</strong> Sometimes a simple long-context prompt beats a complex agentic workflow. Measure before you add complexity.</li>\n</ul>\n<p>Building production RAG is a game of millimeters. It's about cleaning your data, pinning your models and measuring what's happening under the hood.</p>\n\n- Chunk by document structure, not by raw token count.\n- Retrieve broad, then rerank for precision.\n- Pin the embedding model and re-index when it changes.\n- Keep context small: 3 to 5 good chunks inside a latency budget.\n- Combine vector and keyword search, and filter by metadata first.\n- Score every change against a golden set instead of trusting a vibe check.\n\n<p>If your RAG pipeline answers well in the demo but drifts in production, <a href=\"https://ansezz.com/services/\">here's how I help teams fix retrieval and ship it</a>. You can also <a href=\"https://ansezz.com/contact/\">reach out</a> if you're hitting a wall with your architecture.</p>\n<p>What's the one thing currently keeping you from hitting that \"deploy\" button?</p>\n</figure></figure>",
      "date_published": "2026-05-17T00:00:00.000Z",
      "date_modified": "2026-05-17T00:00:00.000Z",
      "tags": [
        "ai",
        "rag",
        "ai",
        "llm",
        "pgvector",
        "production"
      ],
      "image": "https://ansezz.com/blog/7-rag-mistakes-production/hero-374ce1c2.webp"
    },
    {
      "id": "https://ansezz.com/blog/scaling-with-rabbitmq/",
      "url": "https://ansezz.com/blog/scaling-with-rabbitmq/",
      "title": "Scaling with RabbitMQ: why message brokers matter",
      "summary": "Synchronous controllers are how monoliths die. RabbitMQ exchanges and queues, a one-task-at-a-time path to async, and idempotent Laravel workers.",
      "content_html": "<p>If your checkout request waits on a PDF, an email and three third-party APIs, more RAM won't save it. Hand that work to a message broker and answer the user right away.</p>\n\n- Synchronous controllers tie your response time to the slowest secondary task, so one slow API can turn into a 504 for the user.\n- RabbitMQ sits in the middle: producers publish, exchanges route, queues hold and consumers do the work at their own pace.\n- Move one slow task at a time to a queue, scale workers on their own, and make every worker idempotent.\n\n<p>Every time a user hits \"checkout,\" your server generates a PDF, sends a welcome email, updates inventory and calls three third-party APIs.</p>\n<p>If any of those services takes too long to answer, the user sees a 504 gateway timeout.</p>\n<h3>The monolith wall</h3>\n<p>It starts as a small delay, then becomes a bottleneck. Soon you're adding RAM to a problem bigger hardware can't fix.</p>\n<p>When everything is synchronous, one failure in a secondary task breaks the whole user experience. I've watched dashboards turn red during a marketing spike because the database was busy with background reports and couldn't handle new signups.</p>\n<h3>Change how services talk</h3>\n<p>The fix is to change how your services talk to each other: <strong>decoupling</strong>, with a broker like <strong>RabbitMQ</strong>.</p>\n<h2>Why your request path is too crowded</h2>\n<p>In a typical web app we do too much inside the controller. A request arrives and we finish every related task before sending back \"200 OK.\" That's fine with ten users. For a growing SaaS, it breaks.</p>\n<h3>The coffee shop</h3>\n<p>Picture a coffee shop where the person taking your order also grinds the beans, steams the milk and draws the logo on the cup before taking the next order. The line wraps around the block, because the cashier is tightly coupled to the barista's work.</p>\n<p>To scale, the cashier writes the order on a slip and hands it off, and is free for the next customer. That slip is your message. The counter where the slips go is your message broker.</p>\n<figure>\n<h2>How RabbitMQ routes work</h2>\n<p>RabbitMQ is an open-source message broker that sits between the parts of your system. It stores messages and also routes them by rules you define.</p>\n<h3>The four pieces</h3>\n<ul>\n<li><strong>Producers:</strong> your web apps or APIs that create a task.</li>\n<li><strong>Exchanges:</strong> the post office that decides which queue a message goes to, based on rules.</li>\n<li><strong>Queues:</strong> where messages wait until they are processed.</li>\n<li><strong>Consumers:</strong> background workers (often in Docker containers) that do the work.</li>\n</ul>\n<p>With RabbitMQ in the middle, the web tier only has to say \"this task needs doing.\" That takes milliseconds. The user gets an instant confirmation while workers handle the heavy work when they're ready.</p>\n\n  <div>The controller builds the PDF, sends the email, updates inventory and calls three APIs before it responds. One slow API and the user gets a 504.</div>\n  <div>The controller saves the order, publishes `order.placed` and responds. Workers build the PDF and send the email from the queue, and retry if a provider is down.</div>\n\n<h2>Smoothing out the spikes</h2>\n<p>Sudden bursts and noisy neighbors are among the hardest parts of scaling. If an enterprise client uploads a 100,000-row CSV, you don't want the login page to slow down for everyone else.</p>\n<p>That's exactly the work <a href=\"https://ansezz.com/blog/message-queues-document-processing/\">message queues for heavy document processing</a> are built to absorb.</p>\n<h3>Scale the workers, not the app</h3>\n<p>With RabbitMQ, those 100,000 rows become 100,000 messages. Workers process them at a steady pace. If the queue grows, you add worker instances instead of scaling the whole application.</p>\n<p>That's <a href=\"https://ansezz.com/blog/horizontal-vs-vertical-scaling/\">horizontal scaling</a> in its simplest form: workers are decoupled from the web server, so each scales on its own load.</p>\n<h3>RabbitMQ in Laravel</h3>\n<p>Laravel's queue system ships drivers for the database, Amazon SQS, Redis and Beanstalkd. To talk to RabbitMQ directly, you add the community package <code>vladimir-yuldashev/laravel-queue-rabbitmq</code>.</p>\n<p>Your web tier should take the order. Your workers should make the coffee.</p>\n<h2>Moving from sync to async</h2>\n<p>You don't have to rewrite the codebase overnight. Pick one slow, low-risk task, like the \"forgot password\" email or an image resize, and move only that to a queue. Then pick the next one.</p>\n<pre><code>// instead of sending the email directly\n// $emailService-&gt;sendWelcome($user);\n\n// dispatch a job to the RabbitMQ-backed queue\nProcessWelcomeEmail::dispatch($user)-&gt;onQueue('high-priority');\n\n// the user gets a response right away\nreturn response()-&gt;json([\n    'message' =&gt; 'welcome! check your inbox soon.',\n]);\n</code></pre>\n<p>Now if your email provider (SendGrid, Mailgun or similar) has a bad day, your app stays up. The message waits in RabbitMQ until the service is back.</p>\n\nTime every step inside your slowest controller action. Anything the user doesn't need to see in the response, and that takes more than 100 ms, goes on your list of jobs to queue first.\n\n<h2>Building for what comes next</h2>\n<p>A broker-first mindset is the first step <a href=\"https://ansezz.com/blog/monolith-to-microservices/\">from a monolith to microservices</a>. Once your monolith publishes events like <code>order.placed</code> or <code>user.registered</code>, other services can listen without you changing the original code.</p>\n<h3>See the backlog</h3>\n<p>The system also gets easier to observe and debug. The RabbitMQ management UI shows how many tasks are pending and how fast they're being processed, so you stop guessing why the server is slow.</p>\n<p>Already on GCP? The same patterns apply with <a href=\"https://ansezz.com/blog/event-driven-pubsub/\">Google Pub/Sub</a>. Pick the broker that matches your hosting stack, not the trend cycle.</p>\n<figure>\n\nPick one job and run it twice with the same payload in a test. If it sends two emails or charges twice, add an idempotency key (like the order ID) and check it before doing the work.\n\n\n- **Don't block the user.** If a task takes more than 100 ms and the response doesn't need it, it probably belongs in a queue.\n- **Decouple early.** Separate the \"thinking\" (web tier) from the \"doing\" (workers).\n- **Make workers idempotent.** Messages can be delivered more than once, so the same task must be safe to run twice.\n- **Monitor queue depth.** A growing queue is an early sign you need more workers or a downstream service is failing.\n\n<p>Scaling a SaaS is about giving your work room to breathe, and a broker gives you that room. If you're pulling slow work off your request path, <a href=\"https://ansezz.com/services/\">here's how I help teams design queues and workers that scale</a>, or <a href=\"https://ansezz.com/contact/\">tell me about it</a>.</p>\n<p>What's the slowest part of your application right now, and could it run as a background job?</p>\n</figure></figure>",
      "date_published": "2026-05-16T00:00:00.000Z",
      "date_modified": "2026-05-16T00:00:00.000Z",
      "tags": [
        "architecture",
        "messaging",
        "microservices",
        "scaling",
        "laravel",
        "architecture"
      ],
      "image": "https://ansezz.com/blog/scaling-with-rabbitmq/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/event-driven-pubsub/",
      "url": "https://ansezz.com/blog/event-driven-pubsub/",
      "title": "Event-driven architecture with Google Pub/Sub",
      "summary": "Decouple your services or drown in latency. Topics, fan-out, push vs pull, dead-letter queues, and idempotent consumers in a Laravel Pub/Sub blueprint.",
      "content_html": "<p>If one slow service can kill your whole request, your services are glued together. Events let them work apart.</p>\n\n- A Pub/Sub topic broadcasts each event once, and every subscription gets its own copy to process at its own pace.\n- Choose push delivery for serverless endpoints and pull delivery when your workers need control over backpressure.\n- Add a dead-letter queue, idempotent consumers and a watch on the unacked message count before you call it production-ready.\n\n<p>Building a modern web application usually starts simple. You get a request and you send a response.</p>\n<p>As your business grows, that simple flow starts to feel heavy. Maybe you need to send a welcome email, update a CRM and trigger a data warehouse sync all at once.</p>\n<h3>The cost of tight coupling</h3>\n<p>If you do this synchronously, your users are stuck staring at a loading spinner. If one service fails, the whole request dies, and your system becomes a house of cards.</p>\n<p>Your application logic is tangled like old headphones in a pocket. Every new feature adds more risk and more latency. You need a way to let your services talk without being glued together.</p>\n<h3>Events break the chain</h3>\n<p>The solution is <strong>event-driven architecture</strong> (EDA). In the Google Cloud world, the heart of that architecture is <strong>Google Pub/Sub</strong>.</p>\n<p>It is a globally distributed messaging service that decouples the services that produce events from the services that consume them. If you'd rather self-host the broker, the same patterns translate directly to <a href=\"https://ansezz.com/blog/scaling-with-rabbitmq/\">scaling with RabbitMQ</a>.</p>\n<h2>Understanding topics and subscriptions</h2>\n<p>At its core, Google Pub/Sub is built on two concepts: <strong>topics</strong> and <strong>subscriptions</strong>.</p>\n<p>I like to think of a topic as a radio station. It broadcasts information out into the void. It doesn't care who is listening or what they do with the music.</p>\n<h3>Subscriptions are the listeners</h3>\n<p>A subscription represents a stream of messages from a specific topic. The publisher only needs to know about the topic. It doesn't need to know if there are ten consumers or zero.</p>\n<h3>Sync to async</h3>\n<p>This is the heart of the <a href=\"https://ansezz.com/blog/synchronous-vs-asynchronous-communication/\">shift from synchronous to asynchronous communication</a>. When a user signs up on your site, you publish a <code>UserSignedUp</code> event to a topic.</p>\n<p>Your main app is done and returns a success message to the user right away. Meanwhile, subscribers pick up that event and do their jobs in the background.</p>\n\n  <div>Signup calls the email API, the CRM and the warehouse sync in a row. One failure kills the request, and the user waits for all three.</div>\n  <div>Signup publishes one `UserSignedUp` event and returns. Email, CRM and sync each pick it up on their own, and a failure in one doesn't touch the others.</div>\n\n<h2>The fan-out pattern</h2>\n<p>Fan-out is one of the most useful patterns in Google Pub/Sub. You publish a single message to a topic, and multiple subscriptions each receive a copy.</p>\n<p>Imagine you run an e-commerce store. When an order is placed, three services need to act:</p>\n<ol>\n<li>An inventory service to update stock levels.</li>\n<li>A shipping service to generate a label.</li>\n<li>An analytics service to track revenue.</li>\n</ol>\n<h3>One message, three copies</h3>\n<p>Instead of your checkout service calling three different APIs, it sends one message to an <code>order-events</code> topic. Three separate subscriptions (inventory, shipping, analytics) each get their own copy.</p>\n<p>They process it at their own pace. If the analytics service is down for maintenance, the shipping label still gets created. The messages wait in the subscription until the service is back online.</p>\n<p>Checkout publishes once and moves on. Every downstream service works on its own clock.</p>\n<figure>\n\nPick one place where a request calls two or more services in a row. Replace those calls with one published event and one subscription per service.\n\n<h2>Pull vs push delivery</h2>\n<p>When you set up a subscription, you decide how you want to receive messages. Google Pub/Sub gives you two main options: <strong>pull</strong> and <strong>push</strong>.</p>\n<h3>Push subscriptions</h3>\n<p>Push subscriptions suit serverless architectures. Google Cloud sends the message to a webhook URL you provide.</p>\n<p>This fits <a href=\"https://ansezz.com/work/\">cloud infrastructure</a> built on Cloud Run or Cloud Functions. It scales automatically and you only pay for what you use. You do have to make sure your endpoint can handle sudden spikes in traffic.</p>\n<h3>Pull subscriptions</h3>\n<p>With pull, your consumer service asks Google Pub/Sub for messages when it is ready. This gives you much more control over backpressure: if your worker is busy, it doesn't ask for more work.</p>\n<p>Pull is the usual choice for long-running services or for tools like Laravel's queue workers. It holds up better for heavy processing where you want to fine-tune concurrency, which is the model I use for <a href=\"https://ansezz.com/blog/message-queues-document-processing/\">message queues in document processing</a>.</p>\n<table>\n<thead>\n<tr>\n<th>Delivery</th>\n<th>How messages arrive</th>\n<th>Good fit</th>\n<th>Watch out for</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Push</strong></td>\n<td>Pub/Sub calls your webhook URL</td>\n<td>Cloud Run, Cloud Functions, serverless</td>\n<td>Sudden traffic spikes on the URL</td>\n</tr>\n<tr>\n<td><strong>Pull</strong></td>\n<td>Your worker asks when it is ready</td>\n<td>Long-running workers, Laravel queue workers</td>\n<td>You manage the workers yourself</td>\n</tr>\n</tbody>\n</table>\n<h2>Building resilient systems with DLQs</h2>\n<p>In a distributed system, things will fail. A database might time out or an external API might be down. If a message can't be processed, you don't want to lose it.</p>\n<p>This is where <strong>dead-letter queues</strong> (DLQs) come in.</p>\n<h3>Move the poison message aside</h3>\n<p>A DLQ is another topic where Google Pub/Sub sends messages that failed to be acknowledged after a set number of attempts. Instead of retrying forever and clogging your main pipeline, the \"poison\" message is moved aside.</p>\n<h3>Inspect, fix, replay</h3>\n<p>I always recommend a DLQ for every important subscription. It acts as a safety net.</p>\n<p>You can build a small dashboard or script to inspect the failed messages, fix the underlying issue and replay them. For Laravel jobs, my <a href=\"https://ansezz.com/tools/php-unserialize/\">PHP unserialize tool</a> turns a failed payload back into readable data. That prevents data loss and keeps your system moving.</p>\n<figure>\n\nAttach a dead-letter topic to your busiest subscription, then write a short script that lists what lands there so you can fix and replay it.\n\n<h2>Integrating Google Pub/Sub with Laravel</h2>\n<p>For the PHP and Laravel ecosystem, integrating Google Pub/Sub is smooth. Laravel ships with solid queue support for Redis and SQS, and the <code>google/cloud-pubsub</code> client lets you tap into GCP's global scale.</p>\n<p>This kind of decoupling is what makes the <a href=\"https://ansezz.com/blog/monolith-to-microservices/\">move from a monolith to microservices</a> manageable instead of terrifying.</p>\n<h3>Publishing a message</h3>\n<p>You can treat Google Pub/Sub as a custom queue driver. Here is how you might publish a message in a typical service class:</p>\n<pre><code>use Google\\Cloud\\PubSub\\PubSubClient;\n\n$pubsub = new PubSubClient([\n    'projectId' =&gt; 'your-gcp-project-id',\n]);\n\n$topic = $pubsub-&gt;topic('user-events');\n\n$topic-&gt;publish([\n    'data' =&gt; json_encode([\n        'user_id' =&gt; 123,\n        'action' =&gt; 'signup',\n    ]),\n    'attributes' =&gt; [\n        'event_type' =&gt; 'UserSignedUp',\n        'priority' =&gt; 'high',\n    ],\n]);\n</code></pre>\n<h3>Filter with attributes</h3>\n<p>Attributes let you filter messages at the subscription level. A subscriber can choose to only receive messages where <code>event_type</code> is <code>UserSignedUp</code>.</p>\n<p>That saves compute and outbound traffic, because your worker never sees the messages it doesn't care about.</p>\n<h2>Monitoring and cost management</h2>\n<p>Set up monitoring from day one. Google Cloud integrates Cloud Monitoring with Google Pub/Sub.</p>\n<p>Keep a close eye on your \"unacked message count.\" If this number keeps climbing, your subscribers can't keep up with the producers.</p>\n<h3>Keep the bill in check</h3>\n<p>Google Pub/Sub is cheap at low volumes, but at millions of messages those bytes add up. Use batching on the publisher side to reduce the number of API calls.</p>\n<p>Also watch message retention. The default is seven days, and you can set anywhere from 10 minutes up to 31 days. Keeping unacknowledged messages for more than 24 hours adds storage cost, so shorten the window if you don't need a long replay.</p>\n\nSet an alert on the unacked message count for each subscription, and check whether your retention period is longer than you actually need.\n\n\n- **Start by finding the \"facts\"** in your system (for example `OrderPlaced`) and turn them into events.\n- **Use the fan-out pattern** to keep your services decoupled and focused on one task.\n- **Always add a dead-letter queue** so failures don't block the main pipeline.\n- **Use message attributes for filtering** at the subscription level.\n- **Make your consumers idempotent.** If they receive the same message twice, it doesn't cause errors or double charges.\n\n<p>This takes more planning up front, and I use it as the backbone of many systems I build because the payoff in stability is worth it. If you want help moving a synchronous flow to events, <a href=\"https://ansezz.com/services/\">here's how I help teams design event-driven systems on Pub/Sub and Laravel</a>, or <a href=\"https://ansezz.com/contact/\">let me know what's stopping you</a> from making the switch.</p>\n<p>Are you still using synchronous API calls for everything, or have you started moving toward an event-driven flow?</p>\n</figure></figure>",
      "date_published": "2026-05-02T00:00:00.000Z",
      "date_modified": "2026-05-02T00:00:00.000Z",
      "tags": [
        "architecture",
        "messaging",
        "cloud-platforms",
        "laravel",
        "scaling",
        "architecture"
      ],
      "image": "https://ansezz.com/blog/event-driven-pubsub/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/coolify-docker-saas-hosting/",
      "url": "https://ansezz.com/blog/coolify-docker-saas-hosting/",
      "title": "Self-hosted SaaS with Coolify and Docker",
      "summary": "Get Heroku-grade DX on your own server. How Coolify and Docker on a cheap VPS deliver one-click databases, automatic SSL, and zero-downtime deploys.",
      "content_html": "<p>You don't have to choose between overpaying for simplicity and overworking for control. Coolify and Docker give you the managed-platform experience on a server you own.</p>\n\n- Managed platforms charge per seat and per usage, and they lock your databases, env vars and build pipelines into their ecosystem.\n- Coolify is an open source, self-hosted control plane (think Heroku or Vercel) built on Docker: git-push deploys, one-click databases and automatic SSL.\n- A production setup adds your own Dockerfile (or Nixpacks), off-box database backups and health checks for zero-downtime deploys.\n\n<p>Shipping a SaaS is hard enough without the constant worry of a \"surprise\" bill from your hosting provider.</p>\n<p>I have seen developers start on a managed platform and find that as soon as they add a second team member or cross a bandwidth threshold, their costs jump.</p>\n<h3>Stuck between two bad options</h3>\n<p>You are stuck choosing between paying a \"convenience tax\" that eats your margins and spending your weekend fighting Nginx configurations and manual SSH commands. It feels like you are either overpaying for simplicity or overworking for control.</p>\n<h3>The lock-in problem</h3>\n<p>It gets worse when you realize most managed platforms are wrappers around the same open source tools you could run yourself. You are paying for a pretty dashboard and an easy git-push flow.</p>\n<p>When you try to leave, you find yourself deep in vendor lock-in. Your databases, environment variables and build pipelines are all tied to a proprietary ecosystem. If the platform goes down or changes its pricing, your business is at the mercy of their support team.</p>\n<h3>A better way</h3>\n<p><strong>Coolify</strong> combined with <strong>Docker</strong> gives you the same developer experience as high-end managed platforms, but on your own infrastructure. You get the one-click deploy feel and a clean dashboard while keeping full control over your servers.</p>\n<p>If you want the business case first, I make it in <a href=\"https://ansezz.com/blog/coolify-self-hosted-saas/\">why I ditch expensive cloud providers for self-hosted SaaS</a>. This guide is the hands-on build.</p>\n<h2>Why the cloud is getting more expensive</h2>\n<p>Early on, a $20-per-month plan feels reasonable. But as you grow, those costs multiply instead of adding up.</p>\n<p>Many platforms now charge per seat. With a team of five engineers, you might pay a hundred dollars a month before you deploy a single line of code.</p>\n<h3>Usage fees you can't predict</h3>\n<p>Then come the usage fees. Bandwidth, image optimization and serverless function execution costs are often opaque and hard to predict.</p>\n<h3>What owning the server changes</h3>\n<p>I have seen stacks move from managed services to a self-hosted Coolify setup and cut the monthly infrastructure bill by about ninety percent, with the same performance and reliability.</p>\n<p>When you own the server, you own the resources. There are no \"hidden\" charges for extra build minutes or database connections.</p>\n\nAdd up last month's hosting invoices and split them into seats, usage fees and base plans. The seat and usage lines are what a self-hosted setup removes.\n\n<h2>What exactly is Coolify?</h2>\n<p>Think of Coolify as an open source, self-hosted version of Heroku or Vercel. It is a control plane that sits on your server and manages everything for you.</p>\n<p>It handles your deployments, your reverse proxies, your SSL certificates and your databases. It turns a raw Linux VPS into a full hosting platform.</p>\n<h3>Built on Docker</h3>\n<p>Coolify is built on Docker, so every application you deploy is containerized. Your environment stays consistent across development, staging and production.</p>\n<p>No more \"it works on my machine\" excuses. If it runs in a container on your laptop, it will run the same way on your Coolify server.</p>\n<p>You keep the git-push workflow. You drop the per-seat bill.</p>\n<h2>Why Docker containerization matters</h2>\n<p>Docker is the quiet engine that makes this whole workflow possible. Instead of installing PHP, Node.js or Python directly on your server, you package them into a container image.</p>\n<p>That gives SaaS founders several key benefits:</p>\n<ol>\n<li><strong>Isolation.</strong> Each app runs in its own sandbox. A memory leak in one app won't crash your entire server.</li>\n<li><strong>Portability.</strong> You can move your containers from Hetzner to DigitalOcean to AWS in minutes.</li>\n<li><strong>Version control.</strong> Your infrastructure is defined as code. You can version your Dockerfile just like your application code.</li>\n<li><strong>Scalability.</strong> Adding more instances of your app is as simple as spinning up another container.</li>\n</ol>\n<h3>Easy handoffs</h3>\n<p>I use Docker on every build I ship. It makes handoffs easy: whoever runs the app next doesn't need to worry about the underlying server configuration, only a Docker-compatible environment.</p>\n<p>Coolify deliberately stays at the single-host Docker layer rather than orchestrating clusters. If you're weighing that trade-off, see <a href=\"https://ansezz.com/blog/docker-vs-kubernetes/\">Docker vs Kubernetes: containers vs orchestration</a>.</p>\n<figure>\n<h2>Setting up your control plane</h2>\n<p>Getting started is simple. Coolify's documented minimum is 2 CPU cores, 2 GB of RAM and 10 GB of free disk, plus extra space for Docker images, volumes and backups. I'd budget 4 GB of RAM if you plan to run more than a couple of apps alongside it.</p>\n<p>I usually recommend Ubuntu for the operating system. Once you have your server, run the single installation command from the Coolify documentation.</p>\n<pre><code># the only command you need to bootstrap Coolify\ncurl -fsSL https://cdn.coollabs.io/coolify/install.sh | sudo bash\n</code></pre>\n<h3>What the installer does</h3>\n<p>The script installs Docker, sets up the Traefik reverse proxy and launches the Coolify dashboard. Within minutes, you can log in to your own private hosting panel.</p>\n<p>From there, connect your GitHub or GitLab account. Coolify listens for webhooks and triggers a new build whenever you push code to your main branch.</p>\n<h3>The same feedback loop</h3>\n<p>It feels magical. You get the same feedback loop as the big-name platforms: build logs in real time and a preview URL for your feature branches. And you do it all on a cheap VPS.</p>\n<h2>Handling databases and state</h2>\n<p>Managing databases is one of the hardest parts of self-hosting. Nobody wants to hand-configure Postgres clusters or worry about backing up Redis instances. Coolify solves this with \"one-click\" services.</p>\n<h3>One-click services</h3>\n<p>You can spin up a Postgres, MySQL, MongoDB or Redis instance in seconds. Coolify generates secure credentials and gives you the connection strings.</p>\n<p>It also handles persistent volumes, so your data stays safe when your container restarts or you update the image.</p>\n<h3>Volumes are not backups</h3>\n<p>Persistent volumes are not a backup strategy, though. Schedule automated dumps and keep them off-box, because <a href=\"https://ansezz.com/blog/replication-vs-backup-laravel/\">replication and backup solve different failure modes</a>.</p>\n<h3>Split out the database as you grow</h3>\n<p>For a SaaS, I usually recommend a dedicated server for your databases once traffic climbs. Coolify supports multi-server setups: one server acts as your control plane and other servers act as worker nodes where your apps and databases live.</p>\n<p>(I cover that scaling pattern in more depth in <a href=\"https://ansezz.com/blog/scaling-with-coolify/\">scaling SaaS with advanced Coolify deployment strategies</a>.)</p>\n\nFor each database you run in Coolify, write down where its last backup lives and when it was taken. If the answer is \"on the same server,\" schedule an off-box dump today.\n\n<h2>Security and SSL by default</h2>\n<p>Security shouldn't be an afterthought. Setting up SSL with Let's Encrypt used to mean cron jobs and manual certificates. With Coolify and Traefik, it is fully automated.</p>\n<h3>Certificates without a terminal</h3>\n<p>When you point a domain to your server and add it to your app configuration, Coolify requests and installs an SSL certificate. It also handles renewal, so your SaaS is always served over HTTPS without you touching a terminal.</p>\n<h3>Secrets out of git</h3>\n<p>Coolify also helps you manage environment variables securely. You don't need to hardcode secrets in your git repository. Define them in the dashboard and they are injected into your containers at runtime.</p>\n<p>This is a standard best practice that many developers skip when they are in a rush. Need a strong value for one of those secrets? My <a href=\"https://ansezz.com/tools/secret-generator/\">secret generator</a> makes one in your browser.</p>\n<h2>A production deployment workflow</h2>\n<p>If you want to do this the \"pro\" way, here is how I structure my deployments.</p>\n<h3>Use a Dockerfile</h3>\n<p>Coolify can detect many frameworks like Laravel or Node.js, but I always recommend writing your own Dockerfile. It gives you total control over the build.</p>\n<p>Use multi-stage builds to shrink your image. That makes deployments faster and saves disk space.</p>\n<h3>Use Nixpacks</h3>\n<p>If you don't want to write a Dockerfile, Coolify supports Nixpacks. It is a tool built by Railway that inspects your code and builds an optimized container image, with detection for 20-plus languages.</p>\n<p>Railway itself has since moved to a successor called Railpack, but Nixpacks remains a solid zero-config option inside Coolify. Coolify also offers Railpack as a beta build pack for Git-based apps.</p>\n<h3>Set up health checks</h3>\n<p>Never deploy without a health check. You want your app to actually respond before the reverse proxy sends traffic to it.</p>\n<p>Coolify lets you define a health check endpoint. If the check fails, the old container keeps running and the new one isn't promoted. This is the foundation of zero-downtime deployments.</p>\n\n  <div>The new container goes live as soon as it starts, even if the app inside is still booting or broken.</div>\n  <div>Traffic only moves when the health endpoint answers. A failed check leaves the old container serving users.</div>\n\n<figure>\n<h2>Is self-hosting right for you?</h2>\n<p>Self-hosting isn't for everyone. If you are a solo developer with no interest in learning how a server works, paying the \"convenience tax\" might be worth it.</p>\n<p>Your time is valuable. If a managed platform saves you five hours of frustration a month, it might pay for itself. My <a href=\"https://ansezz.com/tools/self-hosting-cost-calculator/\">self-hosting cost calculator</a> puts those hours next to the server bill.</p>\n<h3>Owning your stack</h3>\n<p>If you are building a real business, though, you need to understand your stack. Owning your infrastructure is about more than saving money. It is about autonomy: no matter what happens to a specific provider, you can move your business elsewhere quickly.</p>\n<p>Whether the app is an e-commerce engine on Shopify or a custom Laravel application, the goal is the same: high performance and long-term stability.</p>\n\nAdd a `/health` route to your app that checks the database connection, set it as the health check in Coolify, then deploy a deliberately broken build to staging and confirm the old container keeps serving.\n\n\n- **Start with a clean VPS** and install Coolify using the official script.\n- **Containerize your application** with a Dockerfile for full control, or use Nixpacks for zero config.\n- **Use one-click services** for your databases and keep automated backups off the server.\n- **Set up your domain** and let Coolify handle the SSL certificates.\n- **Add health checks** so updates never take the app down.\n\n<p>When you stop worrying about the \"how\" of deployment, you can spend more time on the \"what\" of your product. If you want to move your SaaS onto Coolify without losing a weekend to it, <a href=\"https://ansezz.com/services/\">here's how I help teams set up self-hosted deployments</a>, or <a href=\"https://ansezz.com/contact/\">tell me about it</a> directly.</p>\n<p>What is the one thing stopping you from moving your SaaS to a self-hosted setup today?</p>\n</figure></figure>",
      "date_published": "2026-04-19T00:00:00.000Z",
      "date_modified": "2026-04-19T00:00:00.000Z",
      "tags": [
        "devops",
        "coolify",
        "docker",
        "self-hosting",
        "devops",
        "multi-tenancy",
        "deployment",
        "networking"
      ],
      "image": "https://ansezz.com/blog/coolify-docker-saas-hosting/hero-5ece3790.webp"
    },
    {
      "id": "https://ansezz.com/blog/mcp-context-aware-agents/",
      "url": "https://ansezz.com/blog/mcp-context-aware-agents/",
      "title": "MCP tool-use: building context-aware agents",
      "summary": "Build context-aware agents with MCP. How tools, resources, and prompts let one server talk to any client and retire brittle one-off integrations for good.",
      "content_html": "<p>Every new data source used to mean new glue code for your agent. MCP replaces that glue with one protocol any client can speak.</p>\n\n- MCP is an open standard that puts a clean boundary between the AI client and the servers that expose your data and actions.\n- It has three primitives: tools (model-controlled actions), resources (application-controlled data) and prompts (user-controlled templates).\n- The server is the gatekeeper, so you can limit an agent to specific tables or read-only access, and swap models without rebuilding integrations.\n\n<p>Building context-aware agents is harder than it should be. AI tooling is a mess of fragmented integrations.</p>\n<p>Every time I want to give an LLM access to a new data source or tool, I end up writing custom glue code that breaks the moment an API version changes. We force very capable models to peer through a keyhole when they should have a wide-open window into our data.</p>\n<h3>Plumbing eats your time</h3>\n<p>This fragmentation creates a lot of technical debt. You spend most of your time on plumbing and only a sliver on the agent's actual intelligence.</p>\n<p>Without a shared way to pass context, the model often hallucinates because it lacks real-time data. It gets stuck on \"I don't have access to that\" or, worse, \"I'll guess what that data looks like,\" which leads to unreliable output and a poor user experience.</p>\n<h3>One protocol instead</h3>\n<p>The <strong>Model Context Protocol</strong> (MCP) changes this. It is an open standard created by Anthropic and donated in December 2025 to the Agentic AI Foundation, a Linux Foundation fund co-founded by Anthropic, Block and OpenAI.</p>\n<p>It lets me build agents that connect to any data source through one shared language. If you want the bigger picture, I broke down <a href=\"https://ansezz.com/blog/api-vs-mcp/\">API vs MCP</a> separately.</p>\n<h2>Why MCP matters for developers</h2>\n<p>A recurring hurdle in custom web applications has always been data silos. When I work on <a href=\"https://ansezz.com/work/\">complex technical challenges</a>, the goal is usually to make data actionable.</p>\n<p>Traditional tool use means wiring every schema and function call into each app by hand. With MCP, you define them once in a server, and any client can discover them.</p>\n<h3>A clear boundary</h3>\n<p>MCP acts as a bridge. It draws a clear line between the AI application (the client) and the data sources (the servers).</p>\n<p>That separation means I can swap the underlying model without rebuilding the data integration layer. If I move from Claude to another model that supports MCP, the tools and resources stay the same.</p>\n<h3>Less stuffing, more fetching</h3>\n<p>It also eases the <a href=\"https://ansezz.com/blog/context-window-vs-memory/\">context window</a> problem. Instead of stuffing a huge document into the prompt, I expose it as an MCP resource.</p>\n<p>The model only pulls what it needs, when it needs it. That is more efficient and cheaper, and it gives you agents that know their environment without drowning in it.</p>\n\n  <div>One hand-written wrapper per model and per data source. A model switch or API change means rewriting the integration.</div>\n  <div>One MCP server per data source. Any MCP client can use it, and switching models leaves the tools and resources untouched.</div>\n\n<figure>\n<h2>The three pillars: tools, resources and prompts</h2>\n<p>To build with MCP, start with its three core primitives. They are the building blocks of any context-aware system.</p>\n<table>\n<thead>\n<tr>\n<th>Primitive</th>\n<th>Controlled by</th>\n<th>What it is</th>\n<th>Example</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Tools</strong></td>\n<td>The model</td>\n<td>Actions that change the world</td>\n<td>Write a file, POST to a Shopify API</td>\n</tr>\n<tr>\n<td><strong>Resources</strong></td>\n<td>The app</td>\n<td>Read-only data for grounding</td>\n<td>Product docs for a support agent</td>\n</tr>\n<tr>\n<td><strong>Prompts</strong></td>\n<td>The user</td>\n<td>Templates that guide tasks</td>\n<td>A standard \"summarize this ticket\" flow</td>\n</tr>\n</tbody>\n</table>\n<h3>Tools</h3>\n<p>Tools are model-controlled actions. Giving an agent a tool gives it the ability to change the world, like writing a file to disk or making a POST request to a Shopify API. The model decides when to call the tool based on the user's intent.</p>\n<h3>Resources</h3>\n<p>Resources are application-controlled data. Think of them as read-only files or database entries the agent can inspect. They give the agent grounding: for a support agent, the product documentation would be a resource it can search and read to answer accurately.</p>\n<h3>Prompts</h3>\n<p>Prompts are user-controlled templates that guide the interaction. MCP prompts let me standardize how users work with the agent across platforms, so the model interprets tasks consistently.</p>\n\nList what your agent needs and sort each item into tool, resource or prompt. Anything read-only that you currently paste into the system prompt is a candidate resource.\n\n<h2>Building your first MCP server</h2>\n<p>I prefer TypeScript for MCP servers because of the mature official SDK. The protocol itself is language-agnostic, with official SDKs for Python, Java, Go, C#, Kotlin and more.</p>\n<h3>Use the v2 SDK</h3>\n<p>Version 2 of the TypeScript SDK is the stable line, released alongside the 2026-07-28 spec. It ships as split packages (<code>@modelcontextprotocol/server</code> and <code>@modelcontextprotocol/client</code>), and v1 keeps getting fixes for at least six months.</p>\n<p>Here is a basic server that exposes a weather tool with the high-level <code>McpServer</code> API. If you have a sample tool response, my <a href=\"https://ansezz.com/tools/json-to-typescript/\">JSON to TypeScript and Zod converter</a> gives you the types.</p>\n<pre><code>import { McpServer } from \"@modelcontextprotocol/server\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/server/stdio\";\nimport * as z from \"zod/v4\";\n\nconst server = new McpServer({ name: \"weather-server\", version: \"1.0.0\" });\n\nserver.registerTool(\n  \"get_weather\",\n  {\n    description: \"Get the current weather for a location\",\n    inputSchema: z.object({ location: z.string() }),\n  },\n  async ({ location }) =&gt; {\n    // logic to fetch weather from an API goes here\n    return {\n      content: [{ type: \"text\", text: `It is sunny in ${location}` }],\n    };\n  },\n);\n\nasync function main() {\n  const transport = new StdioServerTransport();\n  await server.connect(transport);\n}\n\nmain();\n</code></pre>\n<h3>Small and modular</h3>\n<p>The snippet shows how simple the protocol is. I define the tool and how to handle the call, and the MCP client handles the rest.</p>\n<p>This modular approach is what I look for when managing <a href=\"https://ansezz.com/work/\">cloud infrastructure</a> or complex backend systems. It is clean and it scales.</p>\n\nCopy the server above, replace the weather logic with one read-only call to a system you use daily, and connect it to an MCP client to watch the model call it.\n\n<h2>Security and the MCP ecosystem</h2>\n<p>Security is a major concern when you give an AI agent access to your data. I have seen many setups where API keys are hardcoded or permissions are far too broad.</p>\n<p>MCP helps with a client-server design where the server controls exactly what is exposed.</p>\n<h3>The server is the gatekeeper</h3>\n<p>You can add fine-grained access control at the server level. For example, an MCP server connected to a database can be limited to specific tables or read-only queries.</p>\n<p>That level of control is essential for enterprise applications.</p>\n<p>Decide what the agent can touch on the server side, and the prompt can't talk it past that line.</p>\n<h3>A broad ecosystem</h3>\n<p>The ecosystem has matured fast. MCP has gone from an Anthropic experiment to an industry standard, and the Agentic AI Foundation has support from Google, Microsoft, AWS, Cloudflare and Bloomberg.</p>\n<p>On the tooling side, clients like Zed, Cursor, VS Code and Claude Code ship MCP support, so AI assistants write better code with real context. For a hands-on walkthrough, see how I <a href=\"https://ansezz.com/blog/claude-mcp-dev-tools/\">connect my own dev tools through Claude MCP</a>.</p>\n<figure>\n<h2>Practical steps for getting started</h2>\n<p>If you want to dive into MCP, I recommend these steps:</p>\n<ol>\n<li><strong>Explore the existing MCP servers</strong> on GitHub. The reference repo ships servers for filesystem access, Git, web fetching and persistent memory. See how they are structured.</li>\n<li><strong>Pick a simple data source you use every day.</strong> It could be your Obsidian notes or a local folder of markdown files. Build a basic server to expose them as resources.</li>\n<li><strong>Use a client like Claude Desktop</strong> to test your server. Watch how the model works with your data, and adjust the tool descriptions so they are clearer for the AI.</li>\n<li><strong>Compose multiple MCP servers</strong> once you are comfortable. Imagine an agent that reads your calendar and then drafts an email based on your upcoming meetings.</li>\n<li><strong>Add the coordination layer</strong> when one agent is no longer enough. MCP connects an agent to its tools; A2A connects it to other agents. The <a href=\"https://ansezz.com/blog/mcp-vs-a2a-vs-acp/\">agent protocol stack</a> breaks down where each one belongs.</li>\n</ol>\n\nGive your first MCP server read-only access only. Add write tools one at a time, each with its own narrow permission.\n\n\n- **MCP replaces one-off glue code** with one protocol between clients and servers.\n- **Tools, resources and prompts** are the three primitives, controlled by the model, the app and the user.\n- **Resources ease context window pressure** because the model fetches data only when needed.\n- **The server is the gatekeeper:** limit tables, scopes and write access there.\n- **Models become swappable** because the tools and resources stay the same.\n\n<p>MCP marks a shift away from \"black box\" agents toward transparent, context-aware assistants you can reason about in production. If you're building agents that need real access to your data, <a href=\"https://ansezz.com/services/\">here's how I help teams design and ship MCP servers</a>, or <a href=\"https://ansezz.com/contact/\">drop me a line</a> to swap notes on real-world MCP server design.</p>\n<p>How are you planning to use MCP in your next project?</p>\n</figure></figure>",
      "date_published": "2026-04-05T00:00:00.000Z",
      "date_modified": "2026-04-05T00:00:00.000Z",
      "tags": [
        "ai",
        "mcp",
        "claude",
        "agentic-ai",
        "llm"
      ],
      "image": "https://ansezz.com/blog/mcp-context-aware-agents/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/agentic-workflows-vibe-coding/",
      "url": "https://ansezz.com/blog/agentic-workflows-vibe-coding/",
      "title": "Vibe coding: the shift to agentic workflows",
      "summary": "MCP, agentic loops, and intent-based engineering. How vibe coding becomes a real architecture pattern, plus the Laravel and MCP stack I run today.",
      "content_html": "<p>Coding used to mean translating intent into syntax, line by line. Agentic workflows flip that: you state the intent, and a loop of tools does the typing.</p>\n\n- Chat-style AI assistants left you as the manual bridge between the model and your system: copy, paste, test, repeat.\n- Agentic loops (intent, reasoning, action, observation, correction) let the model reach your tools directly, mostly through MCP.\n- The human job becomes architecture and review: small scoped MCP servers, stateful agent tools and a sign-off gate before any action runs.\n\n<p>I've spent the last decade building Laravel applications, managing Docker clusters and fine-tuning Shopify stores.</p>\n<p>For most of that time, \"coding\" meant one thing: translating a business requirement into syntax a machine could execute. It was a manual, linear process of writing line by line, debugging stack traces and managing state.</p>\n<p>Recently, the ground has shifted. We're moving away from \"writing code\" and into \"orchestrating intent.\"</p>\n<p>This transition is often playfully called <strong>vibe coding</strong>. The meme hides a real architectural change: a move from sequential instructions to <strong>agentic workflows</strong> powered by protocols like <strong>MCP</strong> (Model Context Protocol).</p>\n<p>If you are new to the idea, my <a href=\"https://ansezz.com/blog/vibe-coding/\">intro to vibe coding</a> covers the philosophy and developer-experience side before this post dives into the architecture.</p>\n<h2>The friction of manual syntax</h2>\n<p>The traditional development lifecycle is full of invisible friction. You have an idea (the \"vibe\"), you break it into tasks, and then you spend 80% of your time fighting syntax, configuration and boilerplate.</p>\n<p>In a standard <strong>Laravel</strong> environment, even a simple feature like an automated reporting tool needs routes, controllers, service classes and database migrations.</p>\n<p>You are the compiler. You are the architect. You are the labor.</p>\n<h3>The \"how\" eats the \"what\"</h3>\n<p>Our cognitive load gets consumed by the \"how\" rather than the \"what.\" We get stuck on <strong>PHP</strong> version compatibility or <strong>Docker</strong> networking issues and lose sight of the actual user value.</p>\n<p>This manual micromanagement doesn't scale with the demands of modern business.</p>\n<h2>The \"black box\" assistant problem</h2>\n<p>When AI first arrived as basic autocomplete, it felt like a shortcut. It wasn't a solution.</p>\n<p>We ended up with what I call \"the Copilot paradox\": the AI suggests code, but you still copy-paste it, test it, find the error and feed it back.</p>\n<h3>A broken feedback loop</h3>\n<p>The AI is a \"black box\" that doesn't know your system. It doesn't know your database schema, your <strong>MCP</strong> servers or your deployment status on <strong>Coolify</strong>.</p>\n<p>You are still the manual bridge between the AI's logic and your local environment.</p>\n<h3>Accelerated manual labor</h3>\n<p>This creates a new kind of fatigue. Instead of writing code, you become a high-speed code reviewer, constantly switching between your editor and a chat window.</p>\n<p>That isn't \"vibe coding.\" It's accelerated manual labor.</p>\n\nFor one day, count how many times you copy code out of a chat window and paste an error back in. Each round trip is a step an agent with tool access could do on its own.\n\n<h2>The fix: agentic workflows and MCP</h2>\n<p>True <strong>vibe coding</strong> shifts your role to high-level system architect.</p>\n<p>That becomes possible through <strong>agentic workflows</strong>: systems that execute tasks in loops instead of only completing text. That jump from a static model to an autonomous actor is exactly the <a href=\"https://ansezz.com/blog/llm-vs-ai-agent/\">LLM vs AI agent</a> distinction.</p>\n<h3>MCP is the connector</h3>\n<p>The breakthrough here is the <strong>Model Context Protocol (MCP)</strong>, created by Anthropic and donated in December 2025 to the Agentic AI Foundation, a Linux Foundation fund, so it is now vendor-neutral.</p>\n<p>MCP acts as the \"USB-C port\" for AI applications. Instead of you handing the AI context, the AI uses an MCP client to talk directly to your tools: your <strong>PostgreSQL</strong> database, your <strong>Slack</strong> channels or your <strong>GitHub</strong> repositories.</p>\n\n  <div>You paste context in, copy code out, run it, paste the error back. You are the bridge between the model and your system.</div>\n  <div>The agent reads the schema, runs the migration, reads the error and fixes it through MCP tools. You approve the plan.</div>\n\n<figure>\n<h2>From chains to loops</h2>\n<p>In a traditional chain, you give a prompt and get a result. In an agentic loop, the architecture looks like this:</p>\n<ol>\n<li><strong>Intent.</strong> You describe the outcome (\"build a Laravel dashboard for my Shopify sales\").</li>\n<li><strong>Reasoning.</strong> The AI (like <strong>Claude</strong>) works out that it needs to see the schema.</li>\n<li><strong>Action.</strong> It uses an <strong>MCP</strong> tool to query the database.</li>\n<li><strong>Observation.</strong> It sees a missing table and decides to create a migration.</li>\n<li><strong>Correction.</strong> If the migration fails, it reads the error and fixes it itself.</li>\n</ol>\n<h3>Intent-based engineering</h3>\n<p>I call this \"intent-based engineering.\" You aren't writing the migration. You are approving the architectural decision.</p>\n<p>Nobody lands here on day one. The jump from chat prompts to loops is a progression I broke into <a href=\"https://ansezz.com/blog/ai-coding-workflow-levels/\">levels of an AI coding workflow</a>, and MCP sits near the top of it.</p>\n<p>Stop typing the migration. Start approving the decision behind it.</p>\n<h2>Implementing the agentic stack</h2>\n<p>As an engineer who values quality, I don't let the \"vibe\" take over without guardrails.</p>\n<p>The loop only stays safe if something verifies what comes out of it. That is the whole argument in <a href=\"https://ansezz.com/blog/testing-ai-generated-code/\">trust is not a QA strategy</a>. Here is how I structure my agentic stack using <strong>Laravel</strong> and <strong>AI</strong> today.</p>\n<h3>1. Defined MCP servers</h3>\n<p>I build small, dedicated <strong>MCP</strong> servers that expose only the tools the AI needs. This keeps the context window clean and the security tight.</p>\n<p>I go deeper on this in <a href=\"https://ansezz.com/blog/claude-mcp-dev-tools/\">Claude MCP dev tools</a> and on designing <a href=\"https://ansezz.com/blog/mcp-context-aware-agents/\">context-aware agents with MCP</a>.</p>\n<pre><code>// Conceptual MCP tool definition in a PHP environment\npublic function defineTools(): array\n{\n    return [\n        'get_database_schema' =&gt; [\n            'description' =&gt; 'Retrieves the structure of the Laravel application tables.',\n            'parameters' =&gt; [],\n        ],\n        'run_artisan_command' =&gt; [\n            'description' =&gt; 'Executes an artisan command safely.',\n            'parameters' =&gt; ['command' =&gt; 'string'],\n        ],\n    ];\n}\n</code></pre>\n\nWrite down the three tools your agent actually needs for one task (for example: read schema, run tests, run one artisan command). Expose only those in your MCP server and leave everything else out.\n\n<h3>2. Stateful loops</h3>\n<p>Instead of one-off chats, I use tools like <strong>Cursor</strong>, <strong>Claude Code</strong> or <strong>Devin Desktop</strong> (the IDE formerly called <strong>Windsurf</strong>) that keep a stateful connection to my local file system.</p>\n<p>The agent can \"see\" the impact of its changes in real time, just like a human developer would.</p>\n<h3>3. The human-in-the-loop (HITL)</h3>\n<p>The most important part of the architecture is the review gate. Even with agentic loops, the human architect must sign off on the \"plan\" before the \"action\" phase.</p>\n<p>This keeps the <strong>PHP</strong> logic on clean architecture principles rather than just \"making it work.\"</p>\n<p>What that gate should actually check is a separate question. I argue for <a href=\"https://ansezz.com/blog/stop-reading-code-ai-review/\">dropping line-by-line reading</a> in favor of specs and automated verification, and I cover approval gates for writes in <a href=\"https://ansezz.com/blog/hitl-gates-for-agent-mutations/\">HITL gates for agent mutations</a>.</p>\n<figure>\n<h2>What this means for founders</h2>\n<p>If you're a founder or a CTO, the lesson is simple: stop hiring for syntax and start hiring for system design. The technical barrier is collapsing, but the architectural stakes are higher than ever.</p>\n<ul>\n<li><strong>Embrace the vibe.</strong> Focus on the intent and the user experience.</li>\n<li><strong>Invest in infrastructure.</strong> Build the <strong>MCP</strong> connections and data pipelines that let AI be effective.</li>\n<li><strong>Think in loops.</strong> Design internal processes so AI can iterate on its own, which removes you as the bottleneck.</li>\n</ul>\n<p>I now build agent-ready systems, whether it's a complex <strong>Shopify</strong> integration or a custom <strong>SaaS</strong>, so the architecture is ready for agents from day one.</p>\n\nPick one recurring engineering chore (a weekly report, a dependency bump) and write it as an intent plus a done condition. If you can't state the done condition, an agent can't close the loop either.\n\n\n- Chat assistants keep you as the copy-paste bridge. Agentic loops remove that step.\n- MCP is the standard connector between agents and your tools.\n- Keep MCP servers small and scoped to the task.\n- Put a human sign-off between the plan and the action.\n- The code might be generated, but the vision is still yours.\n\n<p>If you want to move your team from chat prompts to scoped agent loops on Laravel and MCP, <a href=\"https://ansezz.com/services/\">here's how I help teams design and ship that stack</a>, or <a href=\"https://ansezz.com/contact/\">get in touch</a> directly.</p>\n<p>Are you ready to stop writing code and start orchestrating your intent?</p>\n</figure></figure>",
      "date_published": "2026-03-22T00:00:00.000Z",
      "date_modified": "2026-03-22T00:00:00.000Z",
      "tags": [
        "architecture",
        "vibe-coding",
        "agentic-ai",
        "mcp",
        "claude",
        "laravel",
        "architecture"
      ],
      "image": "https://ansezz.com/blog/agentic-workflows-vibe-coding/hero-08f124e4.webp"
    },
    {
      "id": "https://ansezz.com/blog/why-your-rag-is-failing/",
      "url": "https://ansezz.com/blog/why-your-rag-is-failing/",
      "title": "Why your RAG implementation is failing in production",
      "summary": "Vector-only retrieval quietly breaks production RAG. Hybrid search, BM25, rank fusion, re-rankers and evals: the fixes that make it reliable.",
      "content_html": "<p>When a production RAG system gives wrong answers, check retrieval before you blame the model. Fix how you find and rank context first.</p>\n\n- Vector search finds related ideas but misses exact identifiers like SKUs, error codes and project names, so pure-vector RAG drifts in production.\n- Hybrid search (vectors plus BM25), merged with Reciprocal Rank Fusion, keeps both meaning and exact matches.\n- A cross-encoder re-ranker and regular evaluation (faithfulness, response relevancy, context precision and recall) turn a good demo into a dependable system.\n\n<p>You built a RAG (Retrieval-Augmented Generation) demo. On your machine, with a handful of PDFs, it looked convincing and the answers felt coherent.</p>\n<p>Then you pushed it to production.</p>\n<h3>Where the demo breaks</h3>\n<p>Users report that the LLM is \"hallucinating\" when the real issue is retrieval. Obvious answers go missing even though they're in the docs. Irrelevant chunks surface because they're semantically close but useless.</p>\n<h3>A retrieval design problem</h3>\n<p>If your RAG system feels unreliable, look at retrieval design first. Most production RAG systems fail because they lean too hard on vector search and mistake a strong demo for a dependable system.</p>\n<p>For the broader catalogue of ways this goes wrong, see <a href=\"https://ansezz.com/blog/7-rag-mistakes-production/\">7 mistakes you're making with your production RAG stack</a>. One pattern keeps showing up in my own work: <strong>a demo proves possibility, but production demands precision.</strong></p>\n<h2>The \"vector noise\" trap</h2>\n<p>In a demo, semantic resemblance often feels good enough. In production, \"good enough\" is where failures begin.</p>\n<p>Embeddings are useful. They map text into vectors so you can retrieve by meaning instead of exact wording. Semantic similarity still differs from retrieval accuracy.</p>\n<h3>Strong on concepts, weak on specifics</h3>\n<p>Vector search is good at finding related concepts and weak at specificity.</p>\n<p>If a user searches for <em>\"Project-X-99 deployment logs,\"</em> vector search might return \"Project-A deployment\" or \"logging best practices,\" because they're semantically close. It can miss the exact identifier \"X-99,\" which carries little semantic weight in a high-dimensional space.</p>\n<h3>The model inherits the drift</h3>\n<p>Once retrieval drifts, the LLM inherits the drift. It can't reason its way out of missing or irrelevant context. You pay for tokens that produce confident but unhelpful answers, and users lose trust for a reason that sits one layer below the model.</p>\n<figure>\n<h2>The fix: hybrid search (vector plus BM25)</h2>\n<p>Combine semantic retrieval with lexical precision. That combination is <strong>hybrid search</strong>.</p>\n<h3>What is BM25?</h3>\n<p>BM25 (Best Matching 25, from the Okapi probabilistic retrieval framework) is the standard lexical ranking method behind classic search. It ignores meaning and rewards exact terms by how important they are in a document and across the collection, while accounting for document length and diminishing returns from repeated terms.</p>\n<h3>Why you need both</h3>\n<ul>\n<li><strong>Vector search</strong> handles synonyms, multilingual queries and conceptual matches.</li>\n<li><strong>BM25</strong> handles exact matches: IDs, SKUs, product codes and technical jargon.</li>\n</ul>\n<p>User questions are rarely pure meaning or pure keyword. They're usually a mix. If you're still deciding how to structure retrieval in the first place, <a href=\"https://ansezz.com/blog/vector-search-vs-graph-search/\">vector search vs graph search</a> is a useful companion read.</p>\n<table>\n<thead>\n<tr>\n<th>Query type</th>\n<th>Vector search</th>\n<th>BM25</th>\n<th>Hybrid</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>\"How do I cancel my plan?\"</strong></td>\n<td>Strong</td>\n<td>Decent</td>\n<td>Strong</td>\n</tr>\n<tr>\n<td><strong>\"Error E-4012 on checkout\"</strong></td>\n<td>Often misses the code</td>\n<td>Strong</td>\n<td>Strong</td>\n</tr>\n<tr>\n<td><strong>\"SKU HB-220 return policy\"</strong></td>\n<td>Often misses the SKU</td>\n<td>Strong on the SKU</td>\n<td>Strong</td>\n</tr>\n<tr>\n<td><strong>\"Ways to speed up the store\"</strong></td>\n<td>Strong</td>\n<td>Weak on synonyms</td>\n<td>Strong</td>\n</tr>\n</tbody>\n</table>\n<p>Embeddings capture what a question means. Keywords catch exactly what it names.</p>\n\nPull 20 real queries from your logs that contain an ID, SKU or error code. Run them through your current retriever and count how many return the exact matching document in the top 5. That number is your case for adding BM25.\n\n<h2>Reciprocal Rank Fusion (RRF)</h2>\n<p>Running two retrieval strategies creates a new question: how do you combine them?</p>\n<p>A practical answer is <strong>Reciprocal Rank Fusion (RRF)</strong>, introduced by <a href=\"https://plg.uwaterloo.ca/~gvcormac/cormacksigir09-rrf.pdf\">Cormack, Clarke and Büttcher in 2009</a>. It's simple and reliable, and it doesn't pretend that scores from different retrievers are directly comparable.</p>\n<h3>How it works</h3>\n<ol>\n<li><strong>Score by rank.</strong> For every document returned by either method, compute a rank-based score.</li>\n<li><strong>The formula.</strong> <code>score = 1 / (k + rank)</code>, where <code>rank</code> is the 1-based position in each result list (so the code below adds <code>1</code> to a 0-based index). The constant <code>k</code> (60 in the original paper) keeps top-ranked items from dominating too much.</li>\n<li><strong>Sum it up.</strong> If a document appears in both the vector and BM25 lists, its scores add together.</li>\n<li><strong>Sort.</strong> The documents with the highest combined scores go to the LLM.</li>\n</ol>\n<p>Here's the minimal PHP version I drop into a Laravel service:</p>\n<pre><code>function reciprocalRankFusion(array $resultSets, int $k = 60): array\n{\n    $scores = [];\n\n    foreach ($resultSets as $results) {\n        foreach ($results as $rank =&gt; $docId) {\n            $scores[$docId] = ($scores[$docId] ?? 0.0) + 1 / ($rank + 1 + $k);\n        }\n    }\n\n    arsort($scores);\n\n    return $scores;\n}\n</code></pre>\n<p>A document that is semantically relevant <em>and</em> lexically precise now rises to the top for a reason.</p>\n<h2>The second pass: re-rankers</h2>\n<p>Hybrid search is a strong foundation, but production RAG usually needs one more layer of judgment: a <strong>re-ranker</strong>.</p>\n<h3>Judge the pair</h3>\n<p>A re-ranker like Cohere Rerank or BGE-Reranker is a cross-encoder that reads the query and the document together. Relevance is relational: the question is whether this document answers <em>this</em> question, beyond what it happens to contain.</p>\n<ol>\n<li>Retrieve the top 50 results with hybrid search.</li>\n<li>Pass those 50 through the re-ranker.</li>\n<li>Send only the top 5 to your LLM.</li>\n</ol>\n<p>This cuts context stuffing and improves what reaches the model. It's one of the clearest differences between a demo and a production system that behaves consistently. The extra hop adds latency, which is where a <a href=\"https://ansezz.com/blog/redis-semantic-caching-rag/\">Redis and semantic caching layer</a> earns its keep.</p>\n\n  <div>Embed the query, take the top 5 vector matches and send them straight to the LLM. Exact codes get missed and near-miss chunks fill the context.</div>\n  <div>Run vector and BM25 search, fuse the lists with RRF, re-rank the top 50 with a cross-encoder and send the best 5. Then score the answers every week.</div>\n\n<figure>\n<h2>Your production RAG checklist</h2>\n<p>A RAG system can impress in a demo and still be weak in production. Once real users, messy documents and ambiguous queries arrive, weak retrieval turns the LLM into expensive guesswork.</p>\n<h3>The fixes I rely on</h3>\n<ul>\n<li><strong>Stop relying on vector-only search.</strong> Add a BM25 layer.</li>\n<li><strong>Use RRF.</strong> Fuse lexical and semantic results without fiddly score calibration.</li>\n<li><strong>Tune chunking on purpose.</strong> Chunks that are too small lose context, and chunks that are too large add noise. For technical docs, 512 to 1,024 tokens with 10 to 15% overlap usually works well for me.</li>\n<li><strong>Add a re-ranker.</strong> Refine the final candidates before anything reaches the LLM.</li>\n<li><strong>Evaluate with RAGAS.</strong> Track faithfulness, response relevancy, context precision and context recall instead of trusting intuition.</li>\n</ul>\n\nBuild a test set of 30 real questions with known correct source documents. Run it through RAGAS before and after you add hybrid search and a re-ranker, and keep the scores in your repo so every retrieval change gets measured.\n\n\n- **Retrieval first.** Most \"hallucinations\" in production RAG start with bad context.\n- **Hybrid beats vector-only.** BM25 catches the IDs and codes embeddings miss.\n- **Fuse with RRF.** Rank-based fusion avoids comparing incompatible scores.\n- **Re-rank before you generate.** Fifty candidates in, five good ones out.\n- **Measure it.** RAGAS scores beat gut feel.\n\n<p>Building a RAG demo is easy. Building <em>reliable</em> RAG takes a real understanding of retrieval, ranking and context design. If you're still choosing your foundation, <a href=\"https://ansezz.com/blog/picking-the-right-rag-stack/\">picking the right RAG stack</a> walks through the vector database trade-offs. If your production RAG keeps missing, <a href=\"https://ansezz.com/services/\">here's how I help teams fix retrieval, ranking and evals</a>, or <a href=\"https://ansezz.com/contact/\">get in touch</a> with your war story.</p>\n<p><strong>Where does your own system still behave like a demo when it should be behaving like production?</strong></p>\n</figure></figure>",
      "date_published": "2026-03-08T00:00:00.000Z",
      "date_modified": "2026-03-08T00:00:00.000Z",
      "tags": [
        "ai",
        "rag",
        "ai",
        "vector-search",
        "production"
      ],
      "image": "https://ansezz.com/blog/why-your-rag-is-failing/hero-e94234ba.webp"
    },
    {
      "id": "https://ansezz.com/blog/monolith-to-microservices/",
      "url": "https://ansezz.com/blog/monolith-to-microservices/",
      "title": "Monolith to microservices: a pragmatic guide",
      "summary": "Skip the big-bang rewrite. The strangler fig pattern, anti-corruption layers, and Docker-first steps to move from monolith to microservices safely.",
      "content_html": "<p>A big-bang rewrite can kill the business while the engineers play with new toys. Strangle the monolith one route at a time instead.</p>\n\n- The real problem is coupling, and jumping straight to microservices just moves those dependencies onto the network.\n- The strangler fig pattern puts a routing layer in front, sends one endpoint at a time to a new service, and keeps the monolith as a safety net.\n- Containerize the monolith first, protect new services with an anti-corruption layer, and split only when the monolith hurts more than the complexity tax.\n\n<p>Your monolith is a ticking time bomb, and every feature you add brings the explosion closer.</p>\n<p>I have seen it happen a dozen times. A startup begins with a clean Laravel or Rails app. It is fast, easy and productive. Then the team grows and the code base swells.</p>\n<h3>When it starts to hurt</h3>\n<p>Suddenly, a simple change to the checkout logic breaks the authentication system. Deployments that used to take five minutes now take forty.</p>\n<p>You are no longer scaling your business. You are managing technical debt.</p>\n<h3>The distributed monolith trap</h3>\n<p>This is when most developers start dreaming of microservices: every service isolated, every deployment instant. The reality is often a nightmare.</p>\n<p>Do it wrong and you end up with a distributed monolith. You get all the complexity of networking with none of the benefits of isolation.</p>\n<h3>Pragmatic scaling</h3>\n<p>I skip the \"big bang\" rewrite and use the <strong>strangler fig pattern</strong> to move from monoliths to microservices without losing my mind or my job.</p>\n<p>If you are still deciding whether to split at all, start with <a href=\"https://ansezz.com/blog/monolith-vs-microservices/\">monolith vs microservices</a>. This post assumes you have made that call and want the migration playbook.</p>\n<h2>The problem with the big bang</h2>\n<p>When a monolith gets too heavy, the first reaction is to scrap it. I have seen companies spend two years on a rewrite only to ship a product with half the features of the original.</p>\n<h3>Coupling is the enemy</h3>\n<p>The monolith is a phase. The real problem is coupling.</p>\n<p>When every part of your app knows too much about every other part, you cannot move. If you jump straight into microservices, you will likely port those dependencies into a network layer. Instead of a function call failing, you get a 500 error across a network socket.</p>\n<h3>Extract what hurts most</h3>\n<p>I prefer a slower, deliberate approach focused on high-value extractions. Look for the parts of the app that hurt the most.</p>\n<p>Is image processing slowing down the web server? Is the reporting engine locking up the database? Those are your first candidates for microservices.</p>\n\nList the three parts of your app that cause the most incidents or slow deploys. Pick the one with the fewest dependencies on the rest of the code as your first extraction.\n\n<h2>The strangler fig pattern in practice</h2>\n<p><a href=\"https://martinfowler.com/bliki/StranglerFigApplication.html\">Martin Fowler named this approach</a> after a tree that grows around another tree. It starts as a small vine and eventually replaces the host entirely.</p>\n<p>In software, you build new features as services while the old monolith keeps running.</p>\n<h3>Put a router in front</h3>\n<p>The process starts with an API gateway or a load balancer. I put a routing layer in front: <a href=\"https://www.nginx.com/\">Nginx</a> or a <a href=\"https://cloud.google.com/\">Google Cloud</a> HTTP(S) load balancer with <a href=\"https://cloud.google.com/armor\">Cloud Armor</a> added for WAF and DDoS protection.</p>\n<p>If a request comes in for <code>/api/v1/orders</code>, the URL map sends it to the new service. Everything else goes to the old monolith. (If you're unsure which box does what here, see <a href=\"https://ansezz.com/blog/load-balancer-vs-api-gateway/\">load balancer vs API gateway</a>.)</p>\n<h3>Flip back if it fails</h3>\n<p>This lets me test the new service in production with real traffic while the monolith acts as a safety net. If the new service fails, I flip the routing back.</p>\n<p>I don't have to migrate everything at once. I can migrate one endpoint at a time.</p>\n<p>Move one route, watch it under real traffic, and keep the old path one switch away.</p>\n<figure>\n\n  <div>Freeze features, rebuild everything for months or years, then switch over in one risky launch.</div>\n  <div>Route one endpoint to a new service, prove it with real traffic, then move the next. The monolith shrinks while the business keeps shipping.</div>\n\n<h2>Containerization with Docker</h2>\n<p>You cannot do microservices without <a href=\"https://www.docker.com/\">Docker</a>. I treat every service as a black box.</p>\n<p>The monolith might run on an old version of PHP while the new service is a lean Go binary or a modern Laravel instance. Docker makes that possible.</p>\n<h3>Containerize the monolith first</h3>\n<p>I start by containerizing the monolith. Even if it stays a monolith for another year, the container forces me to define its environment and makes the infrastructure reproducible.</p>\n<pre><code># a simplified example of a service container\nFROM php:8.4-fpm\n\nWORKDIR /app\nCOPY . /app\n\nRUN apt-get update &amp;&amp; apt-get install -y \\\n    libpq-dev \\\n    &amp;&amp; docker-php-ext-install pdo_pgsql\n\nEXPOSE 9000\nCMD [\"php-fpm\"]\n</code></pre>\n<h3>Scale each service on its own</h3>\n<p>Once the monolith is containerized, I can deploy it to a platform like <a href=\"https://cloud.google.com/kubernetes-engine\">Google Kubernetes Engine (GKE)</a>.</p>\n<p>This is where microservices pay off. I can scale the order service to fifty instances during a sale while keeping the blog service at two.</p>\n\nWrite a Dockerfile for your monolith as it is today and get it running locally with the same PHP version as production. That alone removes a class of \"works on my machine\" bugs.\n\n<h2>Communication and the anti-corruption layer</h2>\n<p>The hardest part of microservices is the data, not the code. Your monolith has a single database, and your microservices should each have their own. So how do they talk?</p>\n<h3>Don't reach into the old database</h3>\n<p>I use an <strong>anti-corruption layer (ACL)</strong>. When I extract a service, I don't let it reach back into the monolith's database. That would be cheating.</p>\n<p>Instead, I create an interface. If the new service needs user data, it asks the monolith through a private API or a message queue like <a href=\"https://cloud.google.com/pubsub\">Google Pub/Sub</a>.</p>\n<h3>Keep the new service clean</h3>\n<p>The new service doesn't care about the legacy app's messy database schema. It only cares about the data it gets through the ACL.</p>\n<p>When the user logic is migrated later, I update the ACL to point to the new user service. For the asynchronous side (letting services react to events instead of calling each other directly), I lean on <a href=\"https://ansezz.com/blog/event-driven-pubsub/\">event-driven Pub/Sub</a> or <a href=\"https://ansezz.com/blog/scaling-with-rabbitmq/\">a RabbitMQ-backed queue</a>.</p>\n<figure>\n<h2>Cloud infrastructure and DevOps</h2>\n<p>Scaling a monolith usually means buying a bigger server. Scaling microservices means managing a fleet of small instances, so I rely on cloud-native tools to manage the complexity.</p>\n<h3>Infrastructure as code</h3>\n<p>I use <a href=\"https://www.terraform.io/\">Terraform</a> to manage my infrastructure as code. That keeps staging and production identical.</p>\n<p>If I need a new database for a service, I define it in code instead of clicking around a dashboard.</p>\n<h3>One pipeline per service</h3>\n<p>For deployments I use tools like <a href=\"https://github.com/features/actions\">GitHub Actions</a> or <a href=\"https://coolify.io/\">Coolify</a>. Every service has its own pipeline.</p>\n<p>If I update the checkout service, I only deploy the checkout service, without worrying about the rest of the system.</p>\n<h2>The hidden costs of microservices</h2>\n<p>I'd be lying if I said this was all sunshine and rainbows. Microservices come with a \"complexity tax\": distributed logging, service discovery and eventual consistency.</p>\n<h3>Only split when it hurts</h3>\n<p>My advice is to move to microservices only when the pain of the monolith is greater than the complexity tax. If your team is three people and your app is simple, stay in the monolith. You will move faster.</p>\n<p>If you are hitting walls every day and your developers are afraid to touch the code, it is time to start strangling.</p>\n\n- **Start with an API gateway** to handle routing.\n- **Containerize your monolith first** to normalize the environment.\n- **Use the strangler fig pattern** to migrate one domain at a time.\n- **Build an anti-corruption layer** to keep new services clean.\n- **Invest in infrastructure as code** early on.\n- **Only split when the monolith starts to hurt** your productivity.\n\n<p>Migration takes time. I have spent months on a single extraction to get it right, because the goal is a system that grows with your business, not a count of services. If you're planning that first extraction, <a href=\"https://ansezz.com/services/\">here's how I help teams move from a monolith to services without a rewrite</a>, or <a href=\"https://ansezz.com/contact/\">tell me about your migration</a>.</p>\n<p>Have you ever tried a \"big bang\" rewrite only to regret it six months later?</p>\n</figure></figure>",
      "date_published": "2026-02-22T00:00:00.000Z",
      "date_modified": "2026-02-22T00:00:00.000Z",
      "tags": [
        "architecture",
        "microservices",
        "scaling",
        "docker",
        "kubernetes",
        "devops",
        "laravel"
      ],
      "image": "https://ansezz.com/blog/monolith-to-microservices/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/laravel-multi-tenancy/",
      "url": "https://ansezz.com/blog/laravel-multi-tenancy/",
      "title": "Laravel multi-tenancy: a scalable SaaS architecture",
      "summary": "Single DB vs multi-DB, global scopes that stop data leaks, stancl/tenancy in production, isolated storage, automated migrations, and a Docker plus Cloud setup.",
      "content_html": "<p>The scary moment in a SaaS launch is when you realize you don't know if customer A can see customer B's data.</p>\n\n- Pick your isolation model early: a shared database with `tenant_id` is cheap, a database per tenant gives the strongest isolation.\n- Never rely on memory to filter tenant data. A global scope trait adds the filter to every query and sets `tenant_id` on create.\n- Keep the web tier stateless and isolate cache and storage per tenant, so you can scale out with Docker and automate tenant setup.\n\n<p>I still remember the panic of my first SaaS launch. I was watching the logs as the third customer signed up, and I realized I had no idea if customer A could see customer B's data.</p>\n<p>That realization is a rite of passage for every developer.</p>\n<h3>Isolation is the hard part</h3>\n<p>Building a software-as-a-service (SaaS) platform is a big technical challenge, and the main hurdle is almost always data isolation. Every tenant should feel like they have the whole application to themselves.</p>\n<p>If you get this wrong early, it will haunt you for a long time. Here is how I approach Laravel multi-tenancy to serve many customers from one codebase while keeping security and scalability intact.</p>\n<h2>Why simple database structures fail SaaS</h2>\n<p>Most developers start with a single database and add a <code>user_id</code> or <code>team_id</code> to every table. It works fine for the first ten users. Then the complexity grows.</p>\n<h3>One missing where clause</h3>\n<p>You add more relationships. You forget a <code>where</code> clause in one obscure controller. Suddenly one customer can see another customer's private invoices.</p>\n<p>That is a catastrophic failure. It kills trust and can end your business overnight.</p>\n<h3>Data bloat</h3>\n<p>Performance also suffers. As your database grows to millions of rows, the queries get slower.</p>\n<p>Indexing helps, but it does not solve the underlying problem of data bloat. You need a strategy that isolates data while keeping your infrastructure manageable.</p>\n<h2>Choosing your isolation strategy</h2>\n<p>When I build custom web solutions, I always start by choosing between two paths: a single database or a multi-database setup.</p>\n<h3>Single database</h3>\n<p>The <strong>single database</strong> approach uses a shared schema, and every row has a <code>tenant_id</code>. It is cheap to run and easy to update.</p>\n<p>I recommend it for startups where costs need to stay low. You can manage thousands of small tenants on a single server this way.</p>\n<h3>Database per tenant</h3>\n<p>The <strong>multi-database</strong> approach is the gold standard for enterprise. Every customer gets their own database.</p>\n<p>This gives the strongest isolation and makes per-tenant <a href=\"https://ansezz.com/blog/replication-vs-backup-laravel/\">backups and replication</a> straightforward. If one database crashes, the others stay online. I use this for tenants with strict compliance needs.</p>\n<table>\n<thead>\n<tr>\n<th>Strategy</th>\n<th>Isolation</th>\n<th>Cost and effort</th>\n<th>Good fit</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Single database</strong></td>\n<td>Shared schema, <code>tenant_id</code></td>\n<td>Low, easy to update</td>\n<td>Startups, thousands of small tenants</td>\n</tr>\n<tr>\n<td><strong>Multi-database</strong></td>\n<td>One database per tenant</td>\n<td>Higher, more to run</td>\n<td>Enterprise, strict compliance</td>\n</tr>\n</tbody>\n</table>\n<h3>The package I reach for</h3>\n<p>I often use the <code>stancl/tenancy</code> package for Laravel. It is a very flexible tool and lets you switch between these strategies as your business grows.</p>\n<p>Some of this work is on my <a href=\"https://ansezz.com/work/\">work page</a>.</p>\n<figure>\n<h2>Building with global scopes</h2>\n<p>The secret to sleeping well at night is automation. I never rely on my memory to filter data. Instead I use Laravel global scopes.</p>\n<p>A global scope automatically adds a filter to every query on a model. It makes sure <code>Customer::all()</code> only returns customers for the current tenant, behind the scenes, so you cannot forget it.</p>\n<p>If tenant filtering depends on someone remembering it, a leak is only a matter of time.</p>\n<h3>The BelongsToTenant trait</h3>\n<p>I create a <code>BelongsToTenant</code> trait and apply it to every model that needs isolation. It handles the filtering and sets the <code>tenant_id</code> when a new record is created.</p>\n<p>It is a simple solution that prevents most accidental data leaks. Here's what that trait looks like in practice:</p>\n<pre><code>&lt;?php\n\nnamespace App\\Concerns;\n\nuse App\\Models\\Tenant;\nuse App\\Scopes\\TenantScope;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\n\ntrait BelongsToTenant\n{\n    protected static function bootBelongsToTenant(): void\n    {\n        static::addGlobalScope(new TenantScope());\n\n        static::creating(function ($model): void {\n            if (! $model-&gt;tenant_id &amp;&amp; app()-&gt;bound('tenant')) {\n                $model-&gt;tenant_id = app('tenant')-&gt;id;\n            }\n        });\n    }\n\n    public function tenant(): BelongsTo\n    {\n        return $this-&gt;belongsTo(Tenant::class);\n    }\n}\n</code></pre>\n<h3>Isolate cache and storage too</h3>\n<p>You also need to isolate your cache and file storage. If two tenants upload a file named <code>logo.png</code>, they should not overwrite each other.</p>\n<p>I configure Laravel to use tenant-specific prefixes for all storage paths. That gives every tenant a true \"sandbox.\"</p>\n\nWrite one test that creates two tenants, logs in as the first, and asserts that `Model::all()` returns zero rows from the second. Run it for every tenant-scoped model.\n\n<h2>Scaling on the cloud</h2>\n<p>Your architecture is only as good as the infrastructure it runs on. I usually deploy my Laravel apps with Docker on cloud providers like Google Cloud or AWS.</p>\n<h3>Stateless web tier</h3>\n<p>Containerization is key. It lets me <a href=\"https://ansezz.com/blog/horizontal-vs-vertical-scaling/\">scale the application horizontally</a>: when traffic spikes, I spin up more web server instances.</p>\n<p>That only works because the web tier stays <a href=\"https://ansezz.com/blog/stateless-vs-stateful-apps/\">stateless</a>. Tenant context is resolved per request, not stored on the box, so the infrastructure stays clean.</p>\n<p>When a single tenant starts pushing serious request volume, I reach for <a href=\"https://ansezz.com/blog/laravel-octane-high-traffic/\">Laravel Octane to handle high traffic</a> before throwing more servers at the problem.</p>\n<h3>Automate tenant setup</h3>\n<p>I use managed database services like Google Cloud SQL. They handle the heavy lifting of backups and scaling.</p>\n<p>For multi-database setups, automated scripts provision a new database whenever a customer signs up. With this \"infrastructure as code\" approach, adding a tenant is a button click away.</p>\n\n  <div>A new customer signs up, and someone creates the database, runs migrations and sets up storage by hand.</div>\n  <div>Signup triggers a script that provisions the database, runs tenant migrations and sets the storage prefix. The customer is live in minutes.</div>\n\n<h2>The tenant switcher experience</h2>\n<p>The final piece is the user interface. Customers who own multiple businesses need a smooth way to move between accounts.</p>\n<p>I build clean dashboards with Vue, with the frontend talking to the Laravel backend over a GraphQL API for a reactive feel. The tenant switcher is always within reach and shows exactly which context you are working in.</p>\n<h3>Make switching feel instant</h3>\n<p>I cache the tenant configuration in the frontend to avoid needless API calls. These small details separate a basic app from a polished one.</p>\n<figure>\n<h2>My Laravel multi-tenancy checklist</h2>\n<p>If you are starting a new project today, here are the steps I recommend:</p>\n<ol>\n<li><strong>Choose your package early.</strong> I prefer <code>stancl/tenancy</code> because of its flexibility. It handles subdomain routing and database switching out of the box.</li>\n<li><strong>Add global scopes right away.</strong> Don't wait until you have ten models. Add the trait to every tenant-scoped model and make it a standard part of your workflow.</li>\n<li><strong>Automate your deployment.</strong> Use Docker from day one so local development matches production. That avoids the \"it works on my machine\" bugs that plague SaaS launches.</li>\n<li><strong>Plan for data migration.</strong> As your schema changes, you need a way to run migrations across hundreds of databases. Tools like <code>stancl/tenancy</code> have built-in commands for this.</li>\n<li><strong>Keep it simple.</strong> Don't build a multi-database setup if you only have five users. Start small and scale as revenue grows.</li>\n</ol>\n\nList every model in your app and mark which ones hold tenant data. Any marked model without the `BelongsToTenant` trait is a leak waiting to happen.\n\n\n- **Choose isolation early:** a shared database for low cost, a database per tenant for strict isolation.\n- **Automate filtering:** a global scope trait on every tenant model stops most leaks.\n- **Isolate cache and storage** with tenant-specific prefixes.\n- **Stay stateless** so you can scale the web tier horizontally.\n- **Automate tenant provisioning and migrations** from the start.\n\n<p><a href=\"https://ansezz.com/services/\">Here's how I help teams architect and ship multi-tenant SaaS on Laravel</a>, and you can always <a href=\"https://ansezz.com/contact/\">get in touch</a>.</p>\n<p>Are you building a SaaS or migrating your current app to a multi-tenant structure, and what is the main technical hurdle you are facing right now?</p>\n</figure></figure>",
      "date_published": "2026-02-08T00:00:00.000Z",
      "date_modified": "2026-02-08T00:00:00.000Z",
      "tags": [
        "laravel",
        "laravel",
        "multi-tenancy",
        "architecture",
        "docker",
        "databases"
      ],
      "image": "https://ansezz.com/blog/laravel-multi-tenancy/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/ai-vs-traditional-development/",
      "url": "https://ansezz.com/blog/ai-vs-traditional-development/",
      "title": "AI vs traditional development: which fits?",
      "summary": "AI vs traditional development: when AI-assisted speed pays off, when traditional engineering is non-negotiable, and the hybrid workflow I favor.",
      "content_html": "<p>\"AI or traditional development?\" is the wrong question. The right one is how much speed, control and risk your business can afford.</p>\n\n- AI-assisted development wins on speed for known patterns: CRUD apps, admin panels, internal tools and first prototypes.\n- Traditional engineering wins on control when business rules are complex, regulated or expensive to get wrong.\n- Most teams should run a hybrid: AI drafts, humans own architecture, logic and the quality gates before merge.\n\n<p>Most teams frame AI vs traditional development as a binary choice. That framing is the first mistake.</p>\n<p>I see it a lot. Teams chase AI because it feels faster, or reject it because it feels messy.</p>\n<p>Both directions land in the same place: rushed systems with weak foundations, or polished systems that ship too late.</p>\n<p>The better move is to understand where each approach wins, where it breaks and where a hybrid gives you the best return. Both work. They solve different problems.</p>\n<h2>AI-powered development: built for speed</h2>\n<p>AI changes how I build software. Instead of writing every repetitive piece by hand, I use tools that understand context, generate scaffolding, speed up testing and remove a lot of drag from delivery.</p>\n<p>I dig into the day-to-day of this in my post on <a href=\"https://ansezz.com/blog/vibe-coding/\">vibe coding</a>, where taste and intent matter more than raw typing speed.</p>\n<h3>The core advantage: speed</h3>\n<p>This is where AI shines.</p>\n<p>For standard workflows, admin panels, CRUD-heavy systems, internal tools and first-pass prototypes, AI can cut a serious amount of time. What used to take weeks can often shrink to days if the scope is clear and the review process is tight.</p>\n<p>That speed usually comes from a few places:</p>\n<ul>\n<li><strong>Automated code generation.</strong> Prompts turn into usable boilerplate and feature drafts.</li>\n<li><strong>Faster testing.</strong> AI can draft test cases and edge-case coverage quickly.</li>\n<li><strong>Debugging support.</strong> It helps narrow down likely failures faster.</li>\n<li><strong>Documentation help.</strong> It turns rough implementation details into clean internal docs.</li>\n</ul>\n<p>How much of that speed you capture depends on where you sit on the <a href=\"https://ansezz.com/blog/ai-coding-workflow-levels/\">AI coding workflow levels</a>. Copy-pasting into a chat window and orchestrating agents against your real codebase are not the same tool.</p>\n<h3>Who benefits most from AI development</h3>\n<p>I lean toward AI-heavy workflows when speed matters more than perfect customization on day one. That usually means:</p>\n<ul>\n<li>Startups trying to reach product-market fit before the runway gets tight.</li>\n<li>Small teams that need more output without more hires.</li>\n<li>Businesses shipping standard features that follow familiar patterns.</li>\n<li>Teams where non-technical stakeholders want to help with discovery and prototyping.</li>\n</ul>\n<p>In those cases, AI acts like a power tool. It does not replace the builder. It makes the first cut much faster.</p>\n\nList the next five features on your roadmap and mark each one \"known pattern\" or \"custom logic.\" Start AI-heavy work only on the first group.\n\n<h2>Where AI-first teams get burned</h2>\n<p>AI is fast at common patterns. It is weaker at deep product nuance, strange business rules and systems that need careful long-term architecture.</p>\n<p>If you skip review, you can ship something that looks finished but behaves like a prototype wearing a production costume.</p>\n<h3>Know which kind of AI you lean on</h3>\n<p>The trade-offs differ between predictive models and generative ones, which I break down in <a href=\"https://ansezz.com/blog/ml-vs-genai/\">machine learning vs generative AI</a>.</p>\n<h3>The debt bill arrives later</h3>\n<p>Some studies of AI-accelerated codebases report more duplicated code, larger pull requests and faster technical-debt buildup. The upfront speed can quietly shift effort into later debugging and rework.</p>\n<p>That is the trap. The fix is treating AI output as a draft, not as truth, the same discipline I argue for in <a href=\"https://ansezz.com/blog/agentic-workflows-vibe-coding/\">the architectural shift to agentic workflows</a>.</p>\n<p>AI output is a first draft. Review is what makes it a product.</p>\n<figure>\n<h2>Traditional development: the control champion</h2>\n<p>Traditional development is slower, but it gives me tighter control over how the system is shaped.</p>\n<p>This is the path I trust most when the business rules are complex, the architecture matters or the cost of failure is high. Every part of the system is designed with intent instead of inferred from a prompt.</p>\n<h3>The core advantage: control</h3>\n<p>Traditional development is better when the software needs precision. That matters for:</p>\n<ul>\n<li><strong>Complex enterprise systems:</strong> lots of moving parts and layered business logic.</li>\n<li><strong>Regulated industries:</strong> where auditability and traceability matter.</li>\n<li><strong>Mission-critical applications:</strong> where downtime or bad behavior is expensive.</li>\n<li><strong>Custom architectures:</strong> where the product does not fit common patterns.</li>\n</ul>\n<h3>The predictability factor</h3>\n<p>One underrated benefit of traditional development is predictability.</p>\n<p>Manual design, explicit code reviews, architecture decisions and planned testing give me a clearer picture of trade-offs. It is like building with blueprints instead of assembling furniture from a photo.</p>\n<p>That slower process often saves time later because fewer assumptions make it into production.</p>\n<h3>The time investment reality</h3>\n<p>The downside is obvious. Manual coding, reviews, debugging, refactoring and testing take time.</p>\n<p>You need stronger engineering talent, and the discipline to keep standards high when deadlines start squeezing the team. You pay for control in time and cost.</p>\n<h2>AI vs traditional development: head-to-head</h2>\n<table>\n<thead>\n<tr>\n<th>Factor</th>\n<th>AI-assisted development</th>\n<th>Traditional development</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Upfront speed</strong></td>\n<td>Often significantly faster</td>\n<td>Standard industry timelines</td>\n</tr>\n<tr>\n<td><strong>Cost structure</strong></td>\n<td>Cheaper to start, debt-prone later</td>\n<td>Higher labor cost, steadier</td>\n</tr>\n<tr>\n<td><strong>Team requirements</strong></td>\n<td>Still needs senior review</td>\n<td>Requires senior expertise</td>\n</tr>\n<tr>\n<td><strong>Customization level</strong></td>\n<td>Strong on common patterns</td>\n<td>Unlimited customization</td>\n</tr>\n<tr>\n<td><strong>Quality assurance</strong></td>\n<td>Fast drafts, human review required</td>\n<td>Manual review from the start</td>\n</tr>\n<tr>\n<td><strong>Risk management</strong></td>\n<td>Variable, depends on review rigor</td>\n<td>Predictable risk factors</td>\n</tr>\n<tr>\n<td><strong>Scalability</strong></td>\n<td>Scales output, not judgment</td>\n<td>Scales with team growth</td>\n</tr>\n</tbody>\n</table>\n<h2>Making the right choice for your business</h2>\n<h3>Choose AI integration when</h3>\n<p>Choose AI when your bottleneck is delivery speed and the work is close to known patterns. That usually applies when:</p>\n<ul>\n<li>Your market window is tight.</li>\n<li>You are building standard business apps like portals, dashboards, e-commerce flows or content systems.</li>\n<li>Your team wants quick prototypes before committing engineering time.</li>\n<li>Your budget is better spent on iteration than on deep custom engineering from day one.</li>\n</ul>\n<h3>Choose traditional development when</h3>\n<p>Choose traditional development when the cost of being wrong is higher than the cost of being slower. That usually means:</p>\n<ul>\n<li>The app needs a unique architecture.</li>\n<li>Compliance and audit trails are mandatory.</li>\n<li>Reliability matters more than release velocity.</li>\n<li>Your team wants direct ownership of code quality and system design.</li>\n</ul>\n<h3>The hybrid strategy</h3>\n<p>This is the option I recommend most often. The strongest teams do not treat this like a religion. They use AI where speed helps and switch to traditional engineering where judgment matters.</p>\n\n  <div>Boilerplate and first drafts, prototypes, repetitive test scaffolding, first versions of docs and support material.</div>\n  <div>Reshaping drafts, rebuilding critical paths, logic and architecture review, and every final technical decision.</div>\n\n<p>The hybrid model works because it treats AI like a junior accelerator, not like an autopilot.</p>\n\nWrite a one-line rule for your repo: which folders or modules AI may draft freely, and which ones always need a senior design review first. Put it in your contributing guide.\n\n<figure>\n<h2>Implementation guidelines</h2>\n<h3>Starting with AI integration</h3>\n<p>If I were introducing AI into an existing team, I would start small:</p>\n<ul>\n<li>Begin with low-risk features.</li>\n<li>Define a review process for all AI-generated code, one that <a href=\"https://ansezz.com/blog/stop-reading-code-ai-review/\">scales past line-by-line reading</a>.</li>\n<li>Choose tools that fit the current workflow.</li>\n<li>Train the team on prompting, verification and code quality checks.</li>\n</ul>\n<h3>Maintaining traditional excellence</h3>\n<p>If the team stays mostly traditional, I would protect the basics:</p>\n<ul>\n<li>Invest in strong senior review.</li>\n<li>Keep documentation current.</li>\n<li>Use clear architecture standards.</li>\n<li>Avoid rushing complex work into fragile implementations.</li>\n</ul>\n<h3>Building hybrid capabilities</h3>\n<p>If the goal is balance, the workflow matters more than the tools:</p>\n<ul>\n<li>Identify which tasks are repetitive and safe to automate.</li>\n<li>Keep humans responsible for architecture and business logic.</li>\n<li>Add quality gates before merge and deployment. <a href=\"https://ansezz.com/blog/testing-ai-generated-code/\">Here is the gate I run on AI-authored PRs</a>.</li>\n<li>Measure outcomes, not only speed.</li>\n</ul>\n\nTrack two numbers for a month on AI-assisted pull requests: time to merge, and bugs found after merge. If the second number rises faster than the first falls, tighten your gates.\n\n<h2>The future-ready approach</h2>\n<p>The teams that will win in 2026 are not the ones that blindly choose AI or reject it.</p>\n<p>They know where speed is enough, where control is non-negotiable and where a hybrid gives them speed without chaos.</p>\n<p>Use AI to remove friction. Use traditional engineering to protect the parts that matter. Combine both when the business needs speed and reliability at the same time.</p>\n\n- Pick by risk, not by trend: known patterns go AI-first, high-stakes logic stays human-led.\n- AI speed comes with a debt bill if you skip review.\n- Traditional engineering buys control and predictability at the cost of time.\n- A hybrid with clear ownership and quality gates gives most teams the best return.\n\n<p>Your development strategy should match your business goals, not the trend cycle. If you're deciding where AI belongs in your delivery process, <a href=\"https://ansezz.com/services/\">here's how I help teams pick and ship the right mix</a>, or <a href=\"https://ansezz.com/contact/\">reach out</a> to talk it through.</p>\n<p>If you had to choose today, which matters more for your next product: speed, control or a hybrid path?</p>\n</figure></figure>",
      "date_published": "2026-01-25T00:00:00.000Z",
      "date_modified": "2026-01-25T00:00:00.000Z",
      "tags": [
        "architecture",
        "ai",
        "architecture",
        "vibe-coding"
      ],
      "image": "https://ansezz.com/blog/ai-vs-traditional-development/hero-b4e70286.webp"
    },
    {
      "id": "https://ansezz.com/blog/scaling-with-coolify/",
      "url": "https://ansezz.com/blog/scaling-with-coolify/",
      "title": "Scaling SaaS with Coolify: deployment strategies",
      "summary": "Move past the single-server trap. Multi-node Coolify, rolling deploys with real health checks, dedicated build servers, and GitHub Actions wiring for production.",
      "content_html": "<p>One server running Coolify, your database, three apps and every build is fine for an MVP. For real customers, split the control plane, the builds and the data apart.</p>\n\n- Run Coolify on its own small server and deploy apps to separate app servers you add over SSH.\n- Rolling updates only protect users when a health check reports real readiness and the app shuts down gracefully.\n- Move builds to a dedicated build server with a registry, keep production data on a managed database, and deploy from CI with a deploy-only token.\n\n<p>You moved your apps off a messy manual VPS and into Coolify. Everything is in one place. Then traffic spikes.</p>\n<p>Hosting your production database, three web apps and a memory-heavy build on a single $10 DigitalOcean droplet is a recipe for trouble.</p>\n<h3>The single-server trap</h3>\n<p>It's fine for a side project. With paying customers, you need a plan for what happens when that one server hits 100% CPU, or when a deploy takes the whole site down for five minutes.</p>\n<p>Coolify can handle larger workloads. You need to set it up the right way.</p>\n<h3>What this guide covers</h3>\n<p>Multi-server nodes, rolling deployments, dedicated build servers and CI wiring. If you're just getting started, my walkthroughs on <a href=\"https://ansezz.com/blog/coolify-docker-saas-hosting/\">Coolify and Docker for SaaS hosting</a> and running a <a href=\"https://ansezz.com/blog/coolify-self-hosted-saas/\">fully self-hosted SaaS on Coolify</a> cover the foundations.</p>\n<h2>Beyond the single server</h2>\n<p>The most common mistake I see is keeping everything on one node. When a build starts, it eats CPU and RAM. The web app lags and the database gets starved.</p>\n<h3>Split control plane from workloads</h3>\n<p>Run the Coolify instance on one small server of its own. That's mission control. Then add separate app servers where your containers live.</p>\n<p>In Coolify, open <strong>Servers</strong>, add a server over SSH, and choose which server each resource deploys to. That gives you <a href=\"https://ansezz.com/blog/horizontal-vs-vertical-scaling/\">horizontal scalability</a>: when one server fills up, add another and point the next app there.</p>\n<p>It's the same separation I use when building <a href=\"https://ansezz.com/work/\">custom web applications</a>, and it keeps one machine from taking everything down.</p>\n<figure>\n<h2>Rolling deploys that don't drop requests</h2>\n<p>Nothing hurts user trust faster than a <a href=\"https://ansezz.com/tools/http-status-codes/\">\"502 Bad Gateway\"</a> every time you push a small CSS fix. Many self-hosted setups kill the old container and then start the new one, and users fall into the gap.</p>\n<h3>How Coolify rolls</h3>\n<p>For Nixpacks, Railpack, Static, Dockerfile and Docker Image apps, Coolify starts the replacement container while the old one keeps running. It waits for the new container's health check, then stops and removes the old one. Docker Compose apps don't get this rolling sequence.</p>\n<p>Without a health check, Coolify treats a container that started as ready, even if the app inside is still booting.</p>\n<h3>The workflow I use</h3>\n<ol>\n<li><strong>Create a readiness endpoint.</strong> In your Laravel, Vue or Node app, add a route like <code>/healthz</code> that returns 200 only when the app can serve real requests.</li>\n<li><strong>Configure the check.</strong> In the app, open <strong>Configuration &gt; Healthcheck</strong>, choose HTTP, set the path to <code>/healthz</code> and an interval of about 5 seconds. The check runs inside the container, so the image needs <code>curl</code> or <code>wget</code>.</li>\n<li><strong>Shut down gracefully.</strong> Under <strong>Configuration &gt; Advanced &gt; Operations</strong>, set the stop grace period (30 seconds by default) to how long the app needs to finish in-flight requests.</li>\n</ol>\n<p>On the default Traefik proxy, unhealthy containers are removed from routing, so traffic stays on the old container until the new one passes.</p>\n\n  <div>The new container starts, Coolify counts it as ready and stops the old one while Laravel is still warming up. Users see errors for a few seconds.</div>\n  <div>`/healthz` fails until the app can serve requests. Coolify keeps the old container live until the new one passes, then gives it time to finish its work.</div>\n\n<p>Coolify's own docs are clear that rolling updates don't guarantee zero downtime. Both versions run side by side for a moment, so migrations, queues and sessions have to work with old and new code.</p>\n\nOpen your app's health check settings in Coolify. If the endpoint returns 200 before the database connection is up, change it to check the database and cache first, then redeploy and watch the logs.\n\n<h2>A dedicated build server</h2>\n<p>Docker builds are heavy. Compiling assets, installing npm packages and building images can push a server's usage through the roof. If that build runs on the server serving your customers, they feel it.</p>\n<h3>How it works</h3>\n<p>Mark a high-CPU server as a build server in Coolify. On deploy, Coolify builds the image there, pushes it to a container registry, and the app server pulls the finished image.</p>\n<h3>What you need</h3>\n<ul>\n<li><strong>A registry:</strong> Docker Hub, GitHub Container Registry or a self-hosted one. Run <code>docker login</code> on the build server, and on the app server too if the registry is private.</li>\n<li><strong>A dedicated machine:</strong> a server marked as a build server can't be a deployment destination.</li>\n<li><strong>Matching CPU architecture:</strong> build on the same architecture your app servers run.</li>\n</ul>\n<p>Your production server never feels the build. It just pulls a ready-to-run image.</p>\n<p>Keep your builds away from your traffic and your data away from your compute.</p>\n<h3>What about the database?</h3>\n<p>Coolify makes \"New Database\" one click, but running production Postgres or MySQL in a container on the same server as your app is risky.</p>\n<p>For production I usually recommend a managed database like AWS RDS or Google Cloud SQL, with backups and point-in-time recovery handled for you.</p>\n<p>In Coolify you pass the connection string as an environment variable. State stays separate from compute, the same <a href=\"https://ansezz.com/blog/stateless-vs-stateful-apps/\">stateless app</a> discipline that makes horizontal scaling possible.</p>\n<h2>CI/CD with deploy webhooks</h2>\n<p>For a professional workflow, code should move from GitHub to production without anyone clicking in the Coolify UI.</p>\n<p>I prefer GitHub Actions. Coolify's GitHub App integration works well, but Actions let you run tests and linting first and trigger the deploy only when everything passes.</p>\n<h3>Wire the webhook</h3>\n<p>Create an API token with only the <strong>deploy</strong> permission under <strong>Keys &amp; Tokens &gt; API Tokens</strong>, and store it as a CI secret. Copy the app's deploy webhook from <strong>Configuration &gt; Webhooks</strong>. On a self-hosted instance, API access must be turned on in <strong>Settings &gt; Advanced</strong>.</p>\n<pre><code>name: deploy to production\non:\n  push:\n    branches:\n      - main\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    steps:\n      - name: trigger coolify webhook\n        run: |\n          curl --fail -X GET \"${{ secrets.COOLIFY_WEBHOOK_URL }}\" \\\n            -H \"Authorization: Bearer ${{ secrets.COOLIFY_TOKEN }}\"\n</code></pre>\n<p>Put your test job before this step and broken code never reaches your servers.</p>\n<figure>\n<h2>Configuration tips</h2>\n<ul>\n<li><strong>Resource limits:</strong> set CPU and memory limits for every app so one leaky container can't take the whole server.</li>\n<li><strong>External backups:</strong> if you run databases inside Coolify, use S3-compatible backups. I use Backblaze B2 or Cloudflare R2. Never rely on local backups alone.</li>\n<li><strong>Docker cleanup:</strong> large images fill disks fast. Use Coolify's automated Docker cleanup or a cron job (my <a href=\"https://ansezz.com/tools/cron-explainer/\">cron explainer</a> checks the schedule).</li>\n<li><strong>External monitoring:</strong> Coolify tells you a container is running. A tool like Better Stack or GlitchTip tells you whether a human can use the site.</li>\n</ul>\n\nList every resource on your busiest Coolify server: apps, databases and builds. Anything that isn't serving traffic is a candidate to move to a build server or a managed service first.\n\n\n- **Split the control plane.** Coolify on its own small server, apps on separate nodes.\n- **Make health checks honest.** Rolling updates are only as safe as your readiness endpoint and graceful shutdown.\n- **Offload builds.** A dedicated build server plus a registry keeps builds off production.\n- **Separate data from compute.** Managed databases for production state.\n- **Deploy from CI.** A deploy-only token and a webhook after your tests pass.\n\n<p>Scaling well means a system that behaves predictably. Whether you're building a Shopify app or a Laravel SaaS, the principles are the same. If you're moving a growing app onto Coolify, <a href=\"https://ansezz.com/services/\">here's how I help teams set up multi-server deploys and CI</a>, or <a href=\"https://ansezz.com/contact/\">drop me a line</a>.</p>\n<p>Have you ever had a build crash your production server in the middle of the day?</p>\n</figure></figure>",
      "date_published": "2026-01-11T00:00:00.000Z",
      "date_modified": "2026-01-11T00:00:00.000Z",
      "tags": [
        "devops",
        "coolify",
        "deployment",
        "devops",
        "docker",
        "self-hosting",
        "ci-cd",
        "scaling"
      ],
      "image": "https://ansezz.com/blog/scaling-with-coolify/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/shopify-liquid-vs-headless/",
      "url": "https://ansezz.com/blog/shopify-liquid-vs-headless/",
      "title": "Shopify Liquid vs. headless: picking the right stack",
      "summary": "Hydrogen looks great on paper, but Liquid still ships more revenue per week. A practical framework for choosing Liquid, headless Hydrogen, or the messy middle.",
      "content_html": "<p>Headless can fix real problems, or hand your team a second system to babysit. Pick the simplest stack that removes your actual bottleneck.</p>\n\n- Liquid with Online Store 2.0 is still the right default for most growth-stage stores: fast to ship, cheap to maintain and native to Shopify.\n- Headless with Hydrogen and Oxygen can be faster, but only with disciplined frontend work, and it turns your storefront into a software product you own.\n- Decide with four questions: is the theme blocking revenue, can the team own a custom storefront, are integrations truly complex, and is slowness a code or an architecture problem?\n\n<p>Your store is making money, but every change feels like surgery on a moving car.</p>\n<p>Traffic is up, orders are up and so is the pressure. The theme layer starts to push back: feature requests turn into fragile hacks, app scripts pile up and mobile performance slips with every install.</p>\n<h3>The pull of headless</h3>\n<p>This is when brands start weighing Shopify Liquid vs. headless. The pitch sounds great: more freedom, better performance, cleaner frontend architecture. It's also where expensive mistakes happen.</p>\n<p>The real question is which stack fits your stage, your team and how you actually operate.</p>\n<h2>Why Liquid still wins for most stores</h2>\n<p>Liquid is the default for a reason. It's tightly integrated with Shopify, fast to ship and much easier to maintain than a custom headless frontend.</p>\n<p>For most growth-stage stores, Liquid gives the best speed to market. Online Store 2.0 made themes far more modular, so a solid theme setup goes a long way before it becomes a blocker.</p>\n<h3>The production van</h3>\n<p>A good Liquid stack is like a well-tuned delivery van. Not flashy, but it moves product reliably, takes updates quickly and doesn't need a pit crew every week.</p>\n<h3>When the theme becomes the ceiling</h3>\n<p>The problem shows up when business needs outgrow theme constraints. Maybe the storefront needs deeply custom interactions. Maybe product data has to come from an ERP, a PIM and a custom backend at once. Maybe merchandising logic is too complex for theme code to stay clean.</p>\n<p>That's usually when headless becomes a serious conversation.</p>\n<figure>\n<h2>Performance is the main driver</h2>\n<p>Performance is the most common reason teams look at headless, and milliseconds matter. On e-commerce storefronts, small delays add up to lower conversion, weaker ad efficiency and a worse mobile experience.</p>\n<h3>What Hydrogen and Oxygen give you</h3>\n<p>Hydrogen is Shopify's headless framework: a React Router app preconfigured for Shopify's APIs. Oxygen is Shopify's serverless hosting for Hydrogen storefronts at the edge.</p>\n<p>A well-built Hydrogen stack gives you finer control over rendering, data loading, caching and frontend execution. That opens the door to a lower LCP and a more responsive storefront.</p>\n<h3>Headless is only as fast as its frontend</h3>\n<p>If the team over-fetches Storefront API data, hydrates too much JavaScript or ships a bloated component tree, the custom storefront can end up slower than a decent Liquid theme. That happens more often than people admit.</p>\n<p>There's also a middle path. <a href=\"https://ansezz.com/blog/shopify-storefront-web-components/\">Shopify storefront web components</a> add dynamic, framework-light commerce to a page without a full headless rebuild.</p>\n<p>A headless storefront is only as fast as the discipline of the team that builds it.</p>\n<h2>The hidden complexity of going headless</h2>\n<p>Going headless splits commerce from presentation. Shopify still handles products, checkout and admin workflows. Your custom frontend handles the customer experience.</p>\n<p>That sounds clean on paper. In practice, you now own two systems instead of one.</p>\n<h3>Everything becomes your job</h3>\n<p>You manage hosting, deployments, caching, GraphQL queries, error handling, observability and integration behavior across services. Every app in the stack has to be checked for API compatibility. If an app only works by injecting snippets into a theme, that convenience is gone.</p>\n<p>In Liquid, many features feel plug and play. In headless, reviews, loyalty, search, subscriptions, personalization and analytics can all turn into custom integration work.</p>\n<p>The same thinking applies to newer surfaces like <a href=\"https://ansezz.com/blog/agentic-commerce-shopify/\">agentic commerce on Shopify</a>, where the store has to expose clean, machine-readable APIs whatever stack you pick.</p>\n<h3>Renting vs building</h3>\n<p>Liquid is renting a well-equipped shop. Headless is designing your own building. You get more freedom, and the plumbing, wiring and maintenance become your problem.</p>\n<table>\n<thead>\n<tr>\n<th>Factor</th>\n<th>Liquid (Online Store 2.0)</th>\n<th>Headless (Hydrogen + Oxygen)</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Time to launch</strong></td>\n<td>Often weeks</td>\n<td>Months, with a bigger team</td>\n</tr>\n<tr>\n<td><strong>App compatibility</strong></td>\n<td>Most apps work out of the box</td>\n<td>Each app needs an API integration</td>\n</tr>\n<tr>\n<td><strong>Performance ceiling</strong></td>\n<td>Good with clean theme code</td>\n<td>Higher, if the frontend is disciplined</td>\n</tr>\n<tr>\n<td><strong>Ongoing ownership</strong></td>\n<td>Theme updates</td>\n<td>A software product to run</td>\n</tr>\n</tbody>\n</table>\n\nList every app installed on your store and mark each one: works through the theme only, has an API, or you could drop it. The \"theme only\" column is the integration work a headless build would add.\n\n<h2>Time and money: the cost of freedom</h2>\n<p>Time and budget usually decide this faster than architecture opinions.</p>\n<p>A custom Liquid theme can often launch in weeks: faster testing, faster iteration and lower cost. If the business mainly needs merchandising flexibility, better UX and cleaner performance hygiene, Liquid usually gives the better return.</p>\n<h3>Headless is an operating model</h3>\n<p>Headless is a different kind of investment. The build takes longer, the team needs stronger frontend engineering and the integration surface is bigger. Maintenance doesn't stop after launch.</p>\n<p>You're paying for a redesign and also taking on a software product. Deployments, monitoring, API changes, dependency updates, caching strategy and developer ownership become part of normal operations. Shopify releases a new API version every quarter, and my <a href=\"https://ansezz.com/tools/shopify-api-versions/\">Shopify API version calendar</a> shows how long the one your storefront uses is supported.</p>\n<p>For many stores that trade isn't worth it yet. Freedom is expensive when the business doesn't fully need it.</p>\n\n  <div>LCP is high, so the team scopes a six-month Hydrogen rebuild and keeps every app and script it already has.</div>\n  <div>The team removes unused apps and scripts, fixes image sizes and trims theme code, then measures again before anyone budgets a rebuild.</div>\n\n<h2>My decision framework</h2>\n<p>I reduce this decision to four practical questions.</p>\n<ol>\n<li><strong>Is the current theme actually blocking revenue?</strong> If conversion is healthy and the pain is taste or minor flexibility, Liquid is probably still right.</li>\n<li><strong>Can the team own a custom storefront long term?</strong> Someone has to own it after launch, every month.</li>\n<li><strong>Are the integration requirements genuinely complex?</strong> If the storefront has to combine Shopify with custom product logic, external systems or a bespoke application layer, headless starts to make sense.</li>\n<li><strong>Is performance a code problem or an architecture problem?</strong> Many slow stores get faster with script cleanup, better images, fewer apps and tighter theme code.</li>\n</ol>\n<p>The simplest stack that fixes the real bottleneck is usually the best one. Building a spaceship to cross the street is still a bad decision. You can see how this plays out across <a href=\"https://ansezz.com/work/\">real Shopify builds I've shipped</a>.</p>\n<figure>\n\nRun PageSpeed Insights on your three highest-traffic templates (home, collection, product) and note the LCP. Then disable your heaviest app script on a duplicate theme and run it again before you decide anything.\n\n\n- **Audit site speed before changing architecture.** If LCP is acceptable, the stack may not be the problem.\n- **Remove app bloat.** Many slow Liquid stores carry too many scripts and too much leftover code.\n- **Map every integration.** What works natively, what depends on theme injection, what would break in headless.\n- **Estimate ownership cost, not only build cost.** Launch is the first invoice.\n- **Look at Hydrogen if the business really needs headless,** and keep the scope tight.\n\n<p>Choosing a stack is a long-term commitment. The right answer solves the current bottleneck without creating three new ones. If you're weighing a rebuild, <a href=\"https://ansezz.com/services/\">here's how I help Shopify brands choose between Liquid, Hydrogen and the middle path</a>, or <a href=\"https://ansezz.com/contact/\">drop me a line</a> for a 30-minute architecture sanity check.</p>\n<p>Are you hitting real technical limits in Shopify, or feeling the pull of a more customizable stack?</p>\n</figure></figure>",
      "date_published": "2025-12-28T00:00:00.000Z",
      "date_modified": "2025-12-28T00:00:00.000Z",
      "tags": [
        "shopify",
        "shopify",
        "hydrogen",
        "performance",
        "architecture"
      ],
      "image": "https://ansezz.com/blog/shopify-liquid-vs-headless/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/picking-the-right-rag-stack/",
      "url": "https://ansezz.com/blog/picking-the-right-rag-stack/",
      "title": "Picking the right RAG stack: vector databases for AI",
      "summary": "pgvector, Pinecone, Weaviate, Qdrant: a 2026 field guide to picking the right vector store for your AI app, with hybrid search and scaling tips.",
      "content_html": "<p>Pick the vector database that fits your scale, your team and the database you already run.</p>\n\n- Default to pgvector if you already run Postgres: no new infrastructure, and your embeddings sit next to your relational data.\n- Pick Pinecone when you want zero ops at large scale, and Weaviate or Qdrant when hybrid search and filtering decide your quality.\n- Cheap wins matter more than the brand: load data before building the index, normalize vectors and keep metadata lean.\n\n<p>You built a chatbot. It works on your machine until you feed it 50,000 internal documents. Then it hallucinates, it slows down, and it pulls a report from three years ago when you asked for last week's.</p>\n<p>Nine times out of ten, the weak link is the RAG stack, and usually the vector database underneath it.</p>\n<h3>The database wall</h3>\n<p>A Retrieval-Augmented Generation (RAG) system sounds like a weekend project. Past the \"hello world\" stage, you hit the database wall.</p>\n<p>Choosing the wrong vector store early leads to high latency, rising cloud costs and a painful migration six months later when your data outgrows the setup.</p>\n<h3>Pick for your scale</h3>\n<p>I've seen teams freeze in front of the number of options in the AI ecosystem. Pick the right tool for your scale and your team.</p>\n<p>Here is the 2026 landscape, so you can stop scrolling and start shipping.</p>\n<h2>Why the database matters in RAG</h2>\n<p>An LLM like Claude or GPT knows a lot but remembers nothing about your data. RAG gives it that memory, and your vector database is the librarian. If the librarian is slow or loses books, the model can't do its job.</p>\n<p>If you're still deciding whether RAG is the right tool at all, versus baking knowledge into the model, start with my breakdown of <a href=\"https://ansezz.com/blog/rag-vs-fine-tuning/\">RAG vs fine-tuning</a>.</p>\n<h3>Three things to look for</h3>\n<ol>\n<li><strong>Latency:</strong> can it find the right chunk in tens of milliseconds? When it can't, a <a href=\"https://ansezz.com/blog/redis-semantic-caching-rag/\">Redis semantic cache in front of the store</a> often helps more than swapping databases.</li>\n<li><strong>Hybrid search:</strong> can it search by meaning (vectors) and by exact keywords (full-text)?</li>\n<li><strong>Developer experience:</strong> how much time will you spend on DevOps?</li>\n</ol>\n<figure>\n<h2>The contenders</h2>\n<h3>pgvector: \"I already have a database\"</h3>\n<p>If you already run <a href=\"https://ansezz.com/\">Postgres for your web applications</a>, pgvector is usually the first stop. It's an extension that adds vector types and indexes to the database you already trust.</p>\n<p>It fits well under about 10 million vectors. You keep ACID guarantees and simple backups, and your relational data stays next to your embeddings. No new infra, no new security review.</p>\n<ul>\n<li><strong>Pros:</strong> zero new infrastructure on Postgres, easy joins with user metadata, wide ecosystem support (Laravel, Django, Node.js).</li>\n<li><strong>Cons:</strong> 100M+ vectors needs serious hardware, and hybrid search means combining it with Postgres full-text search yourself.</li>\n</ul>\n<h3>Pinecone: \"I want zero ops\"</h3>\n<p>Pinecone is a managed, serverless vector database. You don't manage clusters or tune indexes. You send vectors and get results, and you pay for what you use.</p>\n<p>It's the usual pick for teams that want to grow toward a billion vectors without hiring a dedicated DevOps engineer.</p>\n<ul>\n<li><strong>Pros:</strong> fully managed (pick a region and go), low latency, enterprise features like SOC 2.</li>\n<li><strong>Cons:</strong> there is no open-source or self-hosted version. Since August 2026, Enterprise customers can use Bring Your Own Cloud (BYOC) to run the data plane in their own AWS, GCP or Azure account. Costs also climb quickly with heavy read and write volume.</li>\n</ul>\n<h3>Weaviate and Qdrant: hybrid search first</h3>\n<p>If your app needs to blend semantic search with classic keyword search, these two lead. Both are built for vector retrieval from the ground up, and both offer open-source and managed cloud versions.</p>\n<p>Weaviate does hybrid search out of the box. Qdrant, written in Rust, is fast and light on memory.</p>\n<ul>\n<li><strong>Pros:</strong> strong hybrid search (BM25 plus vectors), flexible hosting (self-hosted Docker or managed cloud), good filtering (\"documents from 2025 that mention security\").</li>\n<li><strong>Cons:</strong> more operational overhead than Pinecone, and a new database API to learn.</li>\n</ul>\n<table>\n<thead>\n<tr>\n<th>Option</th>\n<th>Best for</th>\n<th>Hosting</th>\n<th>Hybrid search</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>pgvector</strong></td>\n<td>Teams already on Postgres</td>\n<td>Your Postgres</td>\n<td>With Postgres full-text</td>\n</tr>\n<tr>\n<td><strong>Pinecone</strong></td>\n<td>Zero ops at large scale</td>\n<td>Managed (BYOC on Enterprise)</td>\n<td>Supported</td>\n</tr>\n<tr>\n<td><strong>Weaviate</strong></td>\n<td>Hybrid search and filtering</td>\n<td>Self-hosted or managed</td>\n<td>Built in</td>\n</tr>\n<tr>\n<td><strong>Qdrant</strong></td>\n<td>Fast, memory-efficient search</td>\n<td>Self-hosted or managed</td>\n<td>Built in</td>\n</tr>\n</tbody>\n</table>\n<p>Pick the vector store your team can operate on a bad night, not the one that wins a benchmark.</p>\n<h2>How to choose: the engineering trade-offs</h2>\n<p>Pick the database that matches your constraints.</p>\n<h3>Factor 1: the \"billions\" problem</h3>\n<p>Most startups don't have a billion vectors. They have a few thousand PDFs. In the sub-1M range, pgvector is almost always the right answer.</p>\n<p>If you're building a global legal search engine or a large e-commerce recommendation system, you need the distributed design of Milvus or Pinecone. Don't build a large distributed system for a small amount of data.</p>\n<h3>Factor 2: hybrid search</h3>\n<p>Pure vector search is weak at exact technical terms. Search for \"PHP 8.4 features\" and you might get general PHP articles. Hybrid search blends the meaning of the vector with the precision of a keyword match.</p>\n<p>If search quality is your top metric, look at Weaviate or Qdrant. If your data is highly relational, weigh <a href=\"https://ansezz.com/blog/vector-search-vs-graph-search/\">vector search vs graph search</a> before you commit to pure vectors.</p>\n<h3>Factor 3: the DevOps tax</h3>\n<p>Every new piece of infrastructure is another thing that can break at 3 AM.</p>\n<p>With a small team, lean on managed services like Pinecone or Zilliz. With a strong infra team that wants to save on cloud margins at high scale, self-hosting Qdrant on Coolify or Kubernetes is the move.</p>\n\nCount the vectors you have today and estimate the count in 12 months. If both numbers are under a few million and you already run Postgres, install pgvector and build a prototype before you evaluate anything else.\n\n<h2>pgvector with Laravel</h2>\n<p>Since I work a lot with <a href=\"https://ansezz.com/\">custom web development using Laravel</a>, here is what this looks like in practice. Laravel 13 with the AI SDK (<code>laravel/ai</code>) has native vector columns and a <code>whereVectorSimilarTo</code> query method on Postgres with pgvector.</p>\n<pre><code>// migration: an HNSW index with cosine distance\n$table-&gt;vector('embedding', dimensions: 1536)-&gt;index();\n\n// query: pass the user's question as a string and Laravel embeds it for you\n$results = Document::query()\n    -&gt;select('content')\n    -&gt;whereVectorSimilarTo('embedding', $query, minSimilarity: 0.4)\n    -&gt;limit(5)\n    -&gt;get();\n</code></pre>\n<p>Under the hood this is pgvector's cosine distance operator, <code>&lt;=&gt;</code>. That snippet is the core of a RAG system: find the content, send it to the LLM and get a grounded answer.</p>\n<p>On older Laravel versions, the <code>pgvector/pgvector-php</code> package gives you a <code>Vector</code> cast and a <code>nearestNeighbors</code> scope that do the same job.</p>\n\n  <div>Every chunk stores the full JSON document as metadata. The index grows fast, memory fills up and queries slow down.</div>\n  <div>The index stores the vector, a document ID and the few fields you filter on. The full record stays in Postgres and you join on the ID.</div>\n\n<h2>Three practical tips</h2>\n<p>These will save you weeks of refactoring.</p>\n<h3>Index after the initial load</h3>\n<p>Vector indexes like HNSW are fast to search but slow to insert into. For a large initial load, insert your vectors first, then create the index. The pgvector docs say the same: it's faster to build the index after loading your data.</p>\n<h3>Normalize your vectors</h3>\n<p>Make sure your embedding model and your vector database agree. If you use cosine similarity, normalize your vectors. It keeps scores consistent and prevents odd ranking bugs.</p>\n<h3>Keep the metadata lean</h3>\n<p>Don't store the whole JSON document inside the vector store. Store the vector and an ID, and keep the heavy data in your primary database. The index stays small and fast.</p>\n<p>These are the cheap wins. For the deeper traps (bad chunking, missing reranking, stale embeddings), see the <a href=\"https://ansezz.com/blog/7-rag-mistakes-production/\">7 mistakes you're making with your production RAG stack</a>. To test chunk sizes on your own documents, try my <a href=\"https://ansezz.com/tools/rag-chunk-splitter/\">RAG chunk splitter</a>.</p>\n<figure>\n\nPick your ten most common user questions and run them against your current setup with and without a keyword filter. If the keyword version wins on half of them, you need hybrid search.\n\n<h2>My rule of thumb</h2>\n<p>I've built these systems for <a href=\"https://ansezz.com/work/\">startups and established businesses</a>, and this is how I usually guide them:</p>\n<ul>\n<li><strong>Default to pgvector.</strong> It's the path of least resistance for most web apps.</li>\n<li><strong>Move to Pinecone</strong> if you need high performance and don't want to manage servers.</li>\n<li><strong>Choose Weaviate or Qdrant</strong> if your app depends on hybrid search and metadata filtering.</li>\n</ul>\n\n- **pgvector** is the default for teams already on Postgres.\n- **Pinecone** is the zero-ops option for scale, with BYOC for Enterprise teams that need data in their own cloud.\n- **Hybrid search** (keyword plus vector) usually beats vector search alone.\n- **Keep it simple.** Don't design for billions of vectors when you have thousands.\n\n<p>The right stack is the one that lets you ship your AI features today. If you're choosing one for production, <a href=\"https://ansezz.com/services/\">here's how I help teams design and ship RAG systems</a>, or <a href=\"https://ansezz.com/contact/\">reach out</a> with your war stories.</p>\n<p>What's the hardest problem you've hit with retrieval in your RAG system so far?</p>\n</figure></figure>",
      "date_published": "2025-12-14T00:00:00.000Z",
      "date_modified": "2025-12-14T00:00:00.000Z",
      "tags": [
        "ai",
        "rag",
        "vector-search",
        "pgvector",
        "laravel"
      ],
      "image": "https://ansezz.com/blog/picking-the-right-rag-stack/hero.webp"
    },
    {
      "id": "https://ansezz.com/blog/vibe-coding/",
      "url": "https://ansezz.com/blog/vibe-coding/",
      "title": "Vibe coding: why projects need more than logic",
      "summary": "Taste, intent and feel matter more in the Cursor and Claude era, and here is how to keep the codebase from becoming a ball of mud while you vibe.",
      "content_html": "<p>Users judge software by how it behaves. With AI writing more of the implementation, your taste and your intent decide whether a product feels right.</p>\n\n- \"Vibe coding\" (Andrej Karpathy, February 2025) means describing what you want and letting AI write the code; for professional work, he now prefers the term \"agentic engineering.\"\n- Your value moves toward taste: noticing when a screen feels off and steering the model with feedback on the feel as well as the function.\n- Vibe the surface and harden the core: keep a solid, tested foundation (Laravel, Docker) under an exploratory UI layer so the codebase doesn't turn into a ball of mud.\n\n<p>Most developers are obsessed with logic. We spend years on syntax, query optimization and architecture debates, and we build systems that are technically correct but feel hollow.</p>\n<p>They work, but they don't sing. That gap is what vibe coding tries to close.</p>\n<h3>Users feel the product</h3>\n<p>Your users care about how the software feels, far more than clever recursion: whether a button responds, whether a layout makes sense, whether the flow is calm or stressful.</p>\n<p>Products that win will master that feel, and AI tools make it cheaper to iterate on it than ever.</p>\n<h2>The logic trap</h2>\n<p>For a long time I thought my job was to be a logic machine. If I followed every best practice and wrote the most efficient Laravel code possible, the project would succeed.</p>\n<p>I was wrong.</p>\n<h3>The skeleton and the home</h3>\n<p>Logic is the foundation, the skeleton that keeps the building standing. Nobody wants to live in a skeleton. People want a home with character and warmth, and in software that character comes from the vibe.</p>\n<p>When we focus only on logic, we get software that does what it says on the tin and leaves users feeling nothing. Or worse, it frustrates them, because nobody thought about the friction of a slow button or a confusing layout.</p>\n<figure>\n<h2>Where \"vibe coding\" comes from</h2>\n<p>Andrej Karpathy coined \"vibe coding\" in a February 2025 post: \"fully give in to the vibes, embrace exponentials, and forget that the code even exists.\" He framed it as a mode for throwaway weekend projects.</p>\n<p>In a February 2026 retrospective, he wrote that programming through LLM agents is becoming a default workflow for professionals, \"except with more oversight and scrutiny,\" and named \"agentic engineering\" as his favorite term for that serious version.</p>\n<h3>The sense I mean</h3>\n<p>I use the broader sense the industry picked up: moving from writing code to curating intent with tools like Cursor and Claude. You describe the <em>feel</em> and <em>behavior</em> you want in natural language, and the AI does the heavy lifting on implementation.</p>\n<h3>Taste is the job</h3>\n<p>Your value as an engineer shifts from typing speed to taste: recognizing when an interface feels \"off\" and knowing how to steer the AI to fix it.</p>\n<p>That is really a question of <a href=\"https://ansezz.com/blog/prompt-engineering-vs-context-engineering/\">prompt engineering versus context engineering</a>: giving the model enough of the right context to make good calls on its own.</p>\n<p>On a Shopify store build, the checkout logic matters. The <em>feel</em> of the checkout (smooth transitions, reassuring feedback, clean typography) is what moves conversions.</p>\n<p>When code gets cheap to write, taste becomes the scarce skill.</p>\n<h2>Tools that keep you in flow</h2>\n<p>To work this way you need tools that stay out of your way, so the gap between an idea and a working screen stays small.</p>\n<h3>Describe the feel</h3>\n<p>Cursor changed how I work. Instead of spending twenty minutes on boilerplate for a new Vue component, I describe it in the chat: \"Build me a dashboard widget that feels airy and modern, uses a bento grid layout and gives the user a sense of calm control over their data.\"</p>\n<h3>Give feedback on the feeling</h3>\n<p>The AI generates the code and I review it. If the feel is wrong, instead of fixing it line by line, I talk to the model again: \"This feels too cramped. Give it more white space and make the shadows softer.\"</p>\n<p>That's the heart of vibe coding: a high-level conversation about intent.</p>\n\n  <div>You open the generated component and start tweaking padding values and shadow numbers by hand until it looks less cramped.</div>\n  <div>You tell the model \"this feels cramped, give it more white space and softer shadows,\" review the diff, and keep the version that feels right.</div>\n\n\nPick one screen in your app that feels off. Write three sentences about how it should feel (not what it should contain), give them to your AI editor and compare the result with what you would have changed by hand.\n\n<h2>Guardrails for vibe coding</h2>\n<p>Some of you are thinking: \"This sounds like a recipe for a messy, unmaintainable codebase.\"</p>\n<p>You're right to worry. If you vibe through a project without discipline, you end up with a ball of mud. Experience matters more here, because someone has to decide what the AI may touch.</p>\n<h3>A solid core under the vibe layer</h3>\n<p>Vibe coding works when you shift your focus to where it pays off. Use your experience to build a solid core, so the vibe layer has something safe to sit on.</p>\n<p>For me that core is usually Laravel and Docker. Laravel is built for developer happiness, and it gives me a foundation I trust: authentication, database migrations and API structure.</p>\n<p>With that core in place and covered by tests, I can be exploratory with the frontend and the user experience. I vibe code the top layer because I trust the layer beneath it.</p>\n<h2>Shopify and vibe coding</h2>\n<p>In e-commerce, this approach fits well. Shopify has spent years refining how checkout and the admin feel.</p>\n<h3>Matching the brand</h3>\n<p>When I do <a href=\"https://ansezz.com/work/\">Shopify customization</a>, I write Liquid code and I also try to match the brand's feel. A luxury jewelry brand needs a very different experience from a high-energy fitness store.</p>\n<p>One should feel slow, deliberate and expensive. The other should feel fast, punchy and motivating. You get there by obsessing over the details a logic-only developer skips.</p>\n<figure>\n<h2>How to start vibe coding today</h2>\n<ol>\n<li><strong>Train your taste.</strong> Look at software as an experience. Which apps do you love using, and why: speed, animation, the way buttons respond? Keep a swipe file of great examples.</li>\n<li><strong>Treat AI as a partner.</strong> Use Claude or Cursor to brainstorm whole concepts, beyond autocomplete. Wiring your editor into the rest of your stack with <a href=\"https://ansezz.com/blog/claude-mcp-dev-tools/\">MCP to give the model real context</a> makes this practical. Next comes real <a href=\"https://ansezz.com/blog/agentic-workflows-vibe-coding/\">agentic workflows with MCP and agentic loops</a>, and I mapped the whole path <a href=\"https://ansezz.com/blog/ai-coding-workflow-levels/\">level by level, from gateway prompts to multi-agent orchestration</a>.</li>\n<li><strong>Build a solid core.</strong> Don't let the vibe turn into chaos. Laravel and Docker keep your infrastructure predictable, and the more you trust the foundation, the more you can play with the surface.</li>\n<li><strong>Iterate on the feeling.</strong> Get the feel right first with a messy prototype, then use your technical skills to refactor and harden it.</li>\n<li><strong>Practice user empathy.</strong> For every piece of logic, ask: \"How will this make the user feel?\" If the answer is \"nothing,\" there's more work to do.</li>\n</ol>\n\nBefore your next vibe-coding session, write down the folders the AI may change freely and the ones it may only touch with tests passing. Put that list in your editor's rules file so every session starts with it.\n\n\n- **Feel is part of the spec.** Users judge software by how it behaves and responds.\n- **Taste is the skill to build.** Steering the AI well matters more than typing fast.\n- **Vibe the surface and harden the core.** A tested Laravel and Docker foundation keeps the mess out.\n- **Iterate on feel, then refactor.** Prototype fast, then make it production-grade.\n- **For production, think agentic engineering:** the same speed, with oversight and review.\n\n<p>Generating a function to sort an array is now a commodity. Building an experience that moves people is rare, and that's where engineers can still stand out. If you want to bring AI-assisted building into your team without losing code quality, <a href=\"https://ansezz.com/services/\">here's how I help teams set up AI coding workflows with guardrails</a>.</p>\n<p>What's the one app you use that just \"feels\" right, and what could you borrow from it for your next project?</p>\n</figure></figure>",
      "date_published": "2025-11-30T00:00:00.000Z",
      "date_modified": "2025-11-30T00:00:00.000Z",
      "tags": [
        "ai",
        "vibe-coding",
        "ai",
        "claude",
        "laravel"
      ],
      "image": "https://ansezz.com/blog/vibe-coding/hero-3ef8694d.webp"
    },
    {
      "id": "https://ansezz.com/blog/hello-world/",
      "url": "https://ansezz.com/blog/hello-world/",
      "title": "Hello, world. Yes, another developer blog.",
      "summary": "Why this developer blog exists, what I'll write about (Laravel, AI, Shopify) and why every code sample here is meant to run in production.",
      "content_html": "<p>Most developer blogs read like a LinkedIn post had a baby with a Medium article. This one won't.</p>\n\n- This blog is where I take notes out loud on the Laravel, Shopify and AI work I ship.\n- Expect Laravel internals, AI engineering with Claude and MCP, Shopify Plus patterns and hard-won DevOps lessons.\n- Every code sample is meant to run in production, and you can follow along through RSS with no newsletter or popup.\n\n<p>I'm Anass. I ship Laravel + Shopify + AI for a living, and this is where I take notes out loud.</p>\n<h2>Why now</h2>\n<p>The patterns I keep reaching for don't show up in tutorials. So I'm writing them down.</p>\n<h3>The notes I wish I had</h3>\n<p>Think <a href=\"https://ansezz.com/blog/laravel-octane-high-traffic/\">Laravel Octane</a> at scale, <a href=\"https://ansezz.com/blog/why-your-rag-is-failing/\">RAG done right</a> and <a href=\"https://ansezz.com/blog/agentic-commerce-shopify/\">agentic commerce</a>.</p>\n<p>Each of those took real projects to get right. Writing them down helps me, and it might save you a few late nights.</p>\n<p>Opinions I'll change when proven wrong, backed by code that already runs in production.</p>\n<figure>\n<h2>What you'll find here</h2>\n<ul>\n<li>Laravel internals you actually use day to day</li>\n<li>AI engineering with <a href=\"https://ansezz.com/blog/claude-mcp-dev-tools/\">Claude and MCP</a></li>\n<li>Shopify Plus app patterns</li>\n<li>Architecture and DevOps lessons learned the hard way</li>\n</ul>\n\nSubscribe via [RSS](/rss.xml). No newsletter, no popup, no LinkedIn dance.\n\n<h2>This blog vs the AI-slop blog</h2>\n<table>\n<thead>\n<tr>\n<th>What you get</th>\n<th>This blog</th>\n<th>AI-slop blog</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Real production code</td>\n<td>Yes</td>\n<td>No</td>\n</tr>\n<tr>\n<td>Opinions</td>\n<td>Yes</td>\n<td>No</td>\n</tr>\n<tr>\n<td>GPT-written filler</td>\n<td>No</td>\n<td>Yes</td>\n</tr>\n<tr>\n<td>Personality</td>\n<td>Yes</td>\n<td>No</td>\n</tr>\n</tbody>\n</table>\n<h2>Code you can actually run</h2>\n<p>Every post that needs code gets the syntax treatment: real, runnable, not pseudo-code.</p>\n<pre><code>// app/Actions/AskClaude.php\nuse Prism\\Prism\\Enums\\Provider;\nuse Prism\\Prism\\Facades\\Prism;\n\n$response = Prism::text()\n    -&gt;using(Provider::Anthropic, 'claude-sonnet-5-5')\n    -&gt;withSystemPrompt('You are a senior Laravel engineer.')\n    -&gt;withPrompt('Refactor this controller for clarity.')\n    -&gt;asText();\n</code></pre>\n<figure>\n\nInstall Prism in a Laravel app, add `ANTHROPIC_API_KEY` to your `.env` and run the snippet above against one of your own controllers.\n\n\n- **Notes from real work:** Laravel, Shopify and AI patterns that tutorials skip.\n- **Code that runs:** every sample is written for production, not slides.\n- **No noise:** follow along with RSS, no newsletter or popup.\n\n<p>That's it. First post live. More incoming. If you want this kind of work on your own product, <a href=\"https://ansezz.com/services/\">here's how I help teams ship Laravel, Shopify and AI features</a>.</p>\n<p>Which topic should I write about first: Laravel at scale, RAG, or agentic commerce?</p>\n</figure></figure>",
      "date_published": "2025-11-16T00:00:00.000Z",
      "date_modified": "2025-11-16T00:00:00.000Z",
      "tags": [
        "career",
        "career",
        "laravel",
        "ai"
      ],
      "image": "https://ansezz.com/blog/hello-world/hero.webp"
    }
  ]
}