Inference Routing Is Becoming an Infrastructure Placement Problem

inference placement orchestration — the Inference Execution Plane governing substrate selection across GPU cluster, dedicated silicon, and sovereign on-prem

The request arrives. The model answers. For most teams, everything in between is invisible — a gateway rule, a load balancer entry, maybe a classifier someone wrote three months ago. That worked when inference meant one cluster and one model family. The execution environment was fixed, so the routing decision was trivial.

That assumption is gone. Enterprise inference now spans GPU clusters, dedicated inference silicon, giant-context processors, provider APIs, and sovereign on-premises substrates — each with different physics, different cost models, and different failure domains. Every routing decision is now implicitly a placement decision. And the application layer was never designed to make it correctly.

Inference placement orchestration is the discipline of governing those decisions at the infrastructure level — where the signals, the authority, and the system visibility actually exist.

inference placement orchestration — the Inference Execution Plane governing substrate selection across GPU cluster, dedicated silicon, and sovereign on-prem

Note: Cost-aware model routing asks: which model should answer this request? Infrastructure-aware inference placement asks: where should this request execute — on which substrate, under which topology, within which latency and sovereignty constraints? Those are no longer the same problem. The first is covered in Cost-Aware Model Routing in Production. This post covers the second.

The Routing Frame Is Obsolete

The API gateway inherited inference routing the same way it inherited everything else — by default, because it was already in the path. When inference ran on a single cluster against a single model family, that was fine. The gateway forwarded the request. The cluster handled it. Routing was endpoint selection, and endpoint selection was trivial because there was essentially one endpoint.

That architecture encoded an assumption that no longer holds: that the execution environment is fixed. Once inference spans multiple substrate types, the routing decision stops being about which model handles the request and starts being about which execution environment the request enters. Those are materially different problems.

An API gateway that was designed to balance HTTP traffic across homogeneous nodes is not equipped to arbitrate between a GPU cluster optimized for throughput, a dedicated inference accelerator optimized for deterministic low latency, and a provider API optimized for burst elasticity. The decision variables — physics, cost tier, latency class, failure domain, sovereignty boundary — exist at the infrastructure layer. The gateway doesn’t see them.

The teams that recognize this shift early won’t be scrambling to retrofit placement logic into application code when they add their second substrate type. The ones that don’t will discover the problem the hard way: through latency variance they can’t explain, cost anomalies that don’t map to request volume, and sovereignty violations that the application registered as successful responses.

Inference Placement Orchestration Requires a New Architectural Layer

What’s needed is a formal definition of what has been informally accumulating as scattered decisions across application code, gateway configs, and ad-hoc infrastructure policy.

The Inference Execution Plane is the infrastructure layer responsible for substrate selection, topology awareness, latency SLA enforcement, and execution cost assignment across heterogeneous inference substrates. It is not a product category. It is an architectural function — the placement authority layer that governs where inference workloads execute and under what constraints.

This distinction matters: the Inference Execution Plane is not the inference platform. It is not Kubernetes for AI, inference middleware, or orchestration tooling. Those systems manage the lifecycle of inference services. The Execution Plane governs the placement decisions that determine which service handles which workload, under which infrastructure conditions, within which policy boundaries.

The function exists in every multi-substrate inference deployment today. The problem is that it’s distributed across systems that weren’t designed to own it — application routers with no infrastructure telemetry, gateway configs that hardcode substrate assumptions, and team boundaries that give application engineers authority over decisions that require infrastructure visibility.

Formalizing the Inference Execution Plane as an explicit architectural layer is the first step toward placing that authority correctly.

inference execution plane — placement authority layer governing substrate selection, topology awareness, and latency SLA enforcement

Multi-Substrate Inference Is a Physics Problem Disguised as an API Problem

The most common misread of the inference routing problem is that it’s a software problem — better classifiers, smarter gateways, more granular routing rules. That framing misses the underlying issue.

Multi-substrate inference is fundamentally a physics problem. Each substrate class carries materially different execution characteristics that determine whether a given request belongs there:

Substrate Optimized For Governing Constraint
GPU cluster Throughput, parallel batch Memory bandwidth, tensor parallelism
Dedicated inference silicon (Groq-class) Deterministic low latency Fixed execution timing, queue depth
Giant-context processors (Cerebras-class) Large context windows On-chip SRAM locality
Provider API Burst elasticity External rate limits, egress cost
Sovereign on-premises Compliance boundary Jurisdiction, air-gap policy

These aren’t performance tiers. They’re architecturally distinct execution environments with different physics. Routing a latency-sensitive synchronous request to a throughput-optimized GPU cluster doesn’t just affect response time — it enters a different queuing model, a different memory locality regime, and potentially a different failure domain. The request may succeed. The infrastructure cost of that success may be invisible to the application.

This is what separates inference placement from the training/inference hardware split that became visible at GTC 2026. Hardware fragmentation didn’t create the placement problem — it made it unavoidable. When a single GPU cluster handled everything, substrate arbitration didn’t matter. Once training and inference silicon diverge, and dedicated inference accelerators enter the stack, every request carries an implicit substrate assignment. The question is whether that assignment is made deliberately or by default.

Deterministic networking becomes relevant here at a layer most teams haven’t reached yet: when the interconnect itself is part of the inference execution path, fabric topology becomes a placement input. The request isn’t just selecting a substrate — it’s selecting a path through an interconnect fabric with its own latency characteristics, congestion domains, and saturation thresholds.

This is workload arbitration. Calling it routing undersells the problem.

Application Routers Are Blind to Infrastructure Signals

The practical consequence of application-layer routing is a structural information gap. Application routers are optimized for what they can observe. Infrastructure signals are outside their observability boundary entirely.

The two signal sets don’t overlap:

Application-visible: request type, token count, tenant identity, SLA tier, model preference

Infrastructure-visible: rack congestion, GPU memory pressure, interconnect saturation, inference queue depth, east-west traffic load, regional failover state, reserved vs. burst substrate allocation, power and cooling headroom

The application router optimizes locally against the signals it can see. The infrastructure absorbs the cost of decisions made without the signals it holds. On a single substrate, this gap is manageable. On multi-substrate deployments, it becomes a structural failure mode.

Locality Collapse is the formal name for what happens when this gap goes unaddressed. It is the loss of topology-aware execution locality caused by routing systems that optimize for endpoint availability rather than infrastructure placement efficiency. The consequences compound as the substrate footprint grows: hidden cross-zone bandwidth amplification, interconnect congestion from misrouted workloads, latency variance that doesn’t track request complexity, sovereignty leakage from spillover that the application never flags, and egress multiplication from cross-region routing that was never modeled.

The most acute expression of Locality Collapse is inference spillover: the primary substrate saturates, the router spills traffic to the next available endpoint — typically a provider API — and the application layer registers a successful response. From the application’s perspective, the request resolved. From the infrastructure’s perspective, a sovereignty boundary may have been crossed, egress cost multiplied, latency doubled, and the spillover event went completely unlogged because the routing system had no model of substrate boundaries.

locality collapse and inference spillover — routing without substrate topology awareness causing sovereignty leakage and egress amplification

Placement Authority Migrates to the Infrastructure Control Plane

The core problem is not that application teams lack visibility into infrastructure signals. It’s that application code has no mechanism to arbitrate shared infrastructure scarcity — and multi-substrate inference creates exactly that condition.

When inference runs on shared GPU infrastructure, placement decisions affect every tenant on the cluster. An application router making substrate assignments without infrastructure telemetry cannot know whether it is consuming reserved capacity, triggering burst allocation, contributing to interconnect saturation, or pushing a congested substrate further into degradation. These are infrastructure governance problems. Solving them requires infrastructure authority.

What application teams cannot own:

  • Cross-region execution decisions
  • GPU tier consumption across tenants
  • Sovereignty boundary traversal
  • Congestion avoidance across shared fabric
  • Interconnect utilization policy
  • Reserved vs. burst substrate allocation
  • Power-aware placement under capacity constraints

The correct owner is the infrastructure control plane. The implementation patterns vary — topology-aware placement engines, inference schedulers with live infrastructure telemetry feeds, policy-aware substrate brokers — but the architectural principle is consistent: placement decisions require the global infrastructure visibility that only the platform layer holds.

This is the same migration that has happened at every prior layer of infrastructure complexity. The CI/CD pipeline became the infrastructure control plane for configuration and deployment. The CLI became a governance surface when agentic systems started consuming it. Inference placement is following the same arc — authority is migrating to the layer that has the system visibility to exercise it responsibly.

inference placement authority migrating from application layer to infrastructure control plane

Inference Placement Is the New Load Scheduling

The transition inference placement is undergoing has a direct precedent in infrastructure history.

VMs needed schedulers. When virtual machines proliferated, placement decisions moved out of manual admin processes and into infrastructure-layer schedulers. DRS, SDRS, affinity rules. The application team didn’t own host placement. The platform layer did.

Containers needed orchestrators. When containers scaled beyond what manual placement could manage, Kubernetes took over bin packing, resource negotiation, and topology-aware scheduling. Application teams don’t own node placement. The orchestrator does.

Inference workloads now need topology-aware execution scheduling. The pattern is identical. The execution environments have fragmented. The placement variables — substrate physics, interconnect topology, sovereignty policy, cost tier — exceed what application-layer routing can manage with the signals available to it. The function needs to move.

What’s different this time is the compression. The transition from VMs to schedulers took years. The transition from containers to orchestrators took years. The hardware substrate split in inference happened over months, driven by dedicated silicon entering production alongside GPU clusters at a pace that enterprise planning cycles weren’t designed to absorb.

The Invisible-Until-Scale Failure Mode

Single-substrate inference deployments hide placement problems. The topology is simple, the substrate is fixed, and the routing decision space is small. Multi-substrate deployments expose placement problems as distributed systems problems. The failure mode isn’t a discrete event. It’s variance accumulation.

The first symptoms are almost never outages. They’re measurement anomalies: latency jitter that doesn’t track request complexity, inconsistent token throughput across nominally identical requests, degraded tail latency that looks like infrastructure noise, regional quality divergence that only appears in percentile metrics, inference queue oscillation that smooths out before anyone investigates. By the time the failure mode is clearly legible as a placement problem, it has typically been accumulating for weeks.

This is the same pattern documented in autonomous systems drift — gradual degradation that produces no single alerting event until the accumulated variance crosses a threshold. Inference observability at the execution layer, not just the model output layer, is the prerequisite for catching this early. Without placement-level telemetry, the variance looks like noise. With it, the pattern is identifiable.

The specific failure sequence: a degraded inference node stays in the routing pool because the application router has no infrastructure health signal — only endpoint availability. Traffic continues flowing to a substrate that is congested, throttled, or queued deep. The application sees elevated latency and assumes model load. The infrastructure knows the substrate is saturated. The routing system has no mechanism to act on that knowledge because the signal doesn’t cross the application/infrastructure boundary.

Architect’s Verdict

Inference routing is following the same arc as every other placement problem in enterprise infrastructure. It starts in the application layer because that’s where the first workload lives and the first engineer with a deadline made a decision. It stays there until the complexity of shared infrastructure, heterogeneous substrates, and cross-boundary policy makes application-layer authority untenable.

The organizations that scale AI infrastructure successfully will not be the ones with the best models. They will be the ones that turn inference placement into an infrastructure discipline before scale forces it on them — before the second substrate type enters production, before the first sovereignty incident, before the first multi-tenant GPU contention event exposes the placement logic that was never designed to handle it.

The ones that don’t will eventually discover they built a distributed infrastructure scheduler inside application code — without the telemetry, the placement policy, or the authority to operate it correctly.

Additional Resources

Originally published at rack2cloud.com

I just build a League MBTI Analytics

 Does your real-life personality match your playstyle? I built a tool that translates your 2026 playstyle data into an MBTI Archetype with dynamic share cards!

Instead of a generic text quiz where people lie to make themselves look better, this analyzes how you actually act under pressure. I want to see if the mapping holds up at scale. Did it accurately call out your playstyle flaws or your main champion pool? Roast the UI, critique the data logic, or flame my archetype mapping in the comments—I’ll be hanging out to answer questions and tweak the algorithm live based on your feedback.

İddia: “PlayStation State of Play etkinliğinde Hogwarts Legacy 2 ve yeni God of War oyunu tanıtılabilir”

İddia:

İddia: "PlayStation State of Play etkinliğinde Hogwarts Legacy 2 ve yeni God of War oyunu tanıtılabilir"Sony dün 2 Haziran tarihli yeni “State of Play” etkinliğini duyurdu. 60 dakika süreceği açıklanan etkinlikte “Marvel’s Wolverine” başta olmak üzere PS5’e gelecek çok sayıda oyun hakkında yeni bilgiler verilecek ve görüntüler paylaşılacak. İddiaya göre etkinlikte uzun süredir merakla beklenen Hogwarts Legacy 2 de duyurulabilir. Resmi olarak 2027’de çıkacağı düşünülen oyuna ek olarak, Faye’ye odaklanan yeni bir “God of War” duyurusu da “State of Play” beklentileri arasında yer alıyor.

İddia: “PlayStation State of Play etkinliğinde Hogwarts Legacy 2 ve yeni God of War oyunu tanıtılabilir” yazısı ilk olarak Teknoloji haberleri – LOG üzerinde yayınlandı.

Why I Built My Own Site with Astro, Not WordPress when I use WordPress for a Living

The short version — WordPress is a brilliant tool — it’s also the wrong tool for a site that’s nothing but articles, calculators, and a sitemap. Tinkernotes has no logins, no comments, no checkout, and no database it can’t live without. So it’s an Astro site built as static HTML and served from Cloudflare Pages. The result is a site that costs £0/month to host, ships almost no JavaScript, and can’t be hacked the ways WordPress sites get hacked. If you’re a WP dev sitting on a content project, this is the build I’d push you towards.

I’ve spent eight years shipping WordPress and WooCommerce for a UK agency — custom plugins, themes, gateway integrations, the lot. WordPress pays my mortgage. So the most reasonable question about this site is: why isn’t it WordPress? I’ve had that question from two developers I respect already. Here’s the long answer.

The choice I was expected to make

If you’d asked me to build a content site any year from 2014 to about 2022, the answer was reflex: WordPress. Pick a host, install WP, drop in a block theme or build one, add Yoast for SEO, write. I could have had Tinkernotes live in an afternoon and never thought about it again. That’s not a knock on WordPress — that is WordPress’s superpower. It collapses “publish a website” into a solved problem.

But “I already know how” is a sunk cost, not a reason. So before defaulting, I wrote down what this site actually has to do:

  1. Render articles, reviews, and guides from text I write.
  2. Run two interactive calculators in the browser.
  3. Generate a sitemap, meta tags, and structured data.
  4. Be fast, and stay cheap.

That’s the whole list. Notice what’s not on it: no user accounts, no comments, no shopping cart, no form that writes to a database, no content edited by anyone but me. The moment I saw the list written out, WordPress stopped looking like the obvious answer and started looking like a lot of moving parts solving problems I don’t have.

What WordPress would be carrying that I don’t need

A standard WordPress install is a PHP application that boots on every request, talks to a MySQL database, runs the plugin and theme hook stack, and assembles HTML — then you bolt a caching plugin on top to avoid doing most of that. It’s a dynamic system pretending to be a static one because, for a content site, static is what you actually want.

Everything dynamic about WordPress is also its attack surface and its maintenance bill:

  • The database. A dependency, a backup job, and a thing that can get out of sync.
  • PHP version drift. Hosts deprecate, plugins lag, you babysit the gap.
  • Plugin updates. Yoast, a cache plugin, a security plugin — each one a small recurring risk and another notification.
  • The login page. /wp-admin and /wp-login.php are probed by bots within minutes of going live. Every WP site I’ve ever run shows it in the logs.

None of that is hard for me — I do it for clients every week. But it’s all labour, and it’s labour in service of capabilities this site will never use. Zero of those moving parts make a single article load faster for a reader.

The framing

A blog is read far more often than it’s written. WordPress optimises the writing moment — the dashboard — and pays for it on every read with a PHP and database round-trip. A static site inverts that: do the work once at build time, then serve plain files forever.

The stack I chose, and what each piece does

Nothing here is exotic. It’s the boring, well-trodden static stack — which is exactly the point.

Astro is the framework. I write content as MDX — Markdown with frontmatter, plus the ability to drop components into the prose. Astro renders every page to plain HTML at build time and ships zero JavaScript by default. The interactive bits — the 3-year cost calculator and the hosting cost calculator — are Svelte “islands”: JS loads only for that component, only on the pages that use it. The article you’re reading ships no framework JavaScript at all.

Cloudflare Pages hosts it. I push to main on GitHub, Cloudflare builds the site and serves the static output from its edge network. There’s no origin server to keep patched, because there’s no origin server. The free tier covers unlimited bandwidth and 500 builds a month — comfortably more than a one-person site needs.

Git is the CMS. Content lives in the repo as .mdx files. Every edit is a commit; every commit is a diff, a history, and a one-line rollback. I get drafts via branches and review via pull requests — the workflow I already use for code, now also for prose.

For a developer this is a feature, not a compromise. For a non-technical client it would be a non-starter, and that distinction matters — I’ll come back to it.

What I gave up coming from WordPress

I want to be honest about the trade, because “just use a static site” advice usually isn’t.

What I gained

  • No server to patch, no PHP version to chase, no database to back up
  • Hosting cost is £0/month on Cloudflare Pages — and stays £0 under traffic spikes
  • No /wp-admin to be brute-forced; the attack surface is basically static files
  • Pages render as pre-built HTML — fast without a caching plugin bolted on
  • Content is versioned in Git: real diffs, real history, instant rollback

What it cost me

  • No dashboard — publishing means a commit and a build, not a “Publish” button
  • No mobile editing, no “fix a typo from my phone in the pub” moment
  • A content rebuild runs on every change; fine at this size, slower at thousands of pages
  • The plugin ecosystem is gone — anything WP gives you free, you wire up yourself
  • It is a developer workflow: hand this stack to a non-dev client and it falls over

The last one is the real boundary. WordPress’s enduring genius is that it hands a non-technical person a usable website and the means to run it. The day-to-day editor isn’t meant to touch code. Astro-on-Git assumes the editor is the developer. For Tinkernotes — a personal site I both build and write — that assumption holds perfectly. For most of my agency clients it would be indefensible.

What changed once it was live

Three things, concretely.

Performance came for free. No caching plugin, no CDN to configure, no image optimisation stack to tune. The pages are already static HTML with hashed assets, served from the edge. I set a Lighthouse budget of 95+ and hitting it has mostly been about not breaking it, rather than chasing it.

The security worry just… left. There’s no admin login to harden, no plugin CVE feed to watch, no “is this theme still maintained” question. A static file can’t run a SQL injection. That’s not nothing — a meaningful share of agency emergency work is cleaning up compromised WordPress installs.

Cost went to zero and stayed there. No managed WordPress host, no renewal-rate surprise twelve months in. If a post ever does numbers, Cloudflare’s edge absorbs it on the same free tier — a static file under load is still just a static file. (Renewal-rate games are exactly the trap I dig into in the Kinsta UK review and Hostinger UK review — worth a read if you are hosting WordPress.)

When I’d still reach for WordPress — and I would

This isn’t a conversion story. WordPress is still the right call when the site needs the things WordPress is for:

  • A non-technical person has to publish and manage content independently.
  • The site is genuinely dynamic — accounts, comments, gated content, member areas.
  • It’s a shop. WooCommerce on WordPress is still where I’d start most UK ecommerce builds — the whole Shopify vs WooCommerce comparison exists because that decision is rarely obvious.
  • You need to move fast with a known team and a deep plugin ecosystem behind you.

The mistake isn’t choosing WordPress. The mistake is choosing it by reflex — defaulting to a dynamic CMS for a job that’s pure content, then spending the project budget caching and securing your way back to the static site you could have built directly.

The takeaway

Match the tool to the workload, not to your CV. Tinkernotes is content and calculators with one developer-author, so it’s static HTML on the edge. Your next build might point straight back at WordPress — and that’s fine, as long as you actually looked.

If you want the longer version of who’s writing this and why, the about page covers it. The short version: I still write WordPress for a living. I just stopped assuming it was the answer before I’d heard the question.

More from Tinkernotes:

Hello! I’m a balloon artist who started 3D modeling

Hi, I’m mitarashi6bear 👋
I’m from Japan.

What I do

My main job is balloon art — creating various characters and figures using balloons to entertain people. It’s a craft built around making 3D forms from scratch.

That love of building shapes naturally pulled me toward Blender. I started learning 3D modeling as a hobby, and now I occasionally take small commissions too. Going from physical sculpture to digital felt surprisingly fun — the mindset of “how do I build this shape?” carries over really well.

Why I started making add-ons

Recently I started developing Blender add-ons, and the reason is pretty simple: I kept running into small workflow annoyances that Blender doesn’t handle out of the box.

Things like:

  • Reloading textures from an external app requires clicking through several menus every single time
  • Moving an object’s origin to its bottom center (super common when placing props) takes more steps than it should

So I just… fixed them. Nothing fancy, just small tools to remove the friction from my own workflow. If they’re useful to others, great.

What I’ll be posting here

  • Blender add-on releases and explanations
  • Small Blender tips I pick up while learning
  • Maybe some thoughts on the overlap between physical craft and 3D modeling

I’m still very much learning, so expect a mix of “here’s a tool I made” and “here’s something that confused me for way too long.”

My add-ons are available on Gumroad:
👉 https://epartworks.gumroad.com

Nice to meet you all!

7 Next.js 16 Caching Bugs That Compile Fine and Break Silently in Production

I lost hours debugging a Next.js 16 caching issue that had no error, no warning, and only showed up in production.

The Next.js 16 caching model is genuinely good. But it introduces a class of bugs that are harder to detect than anything in previous versions: bugs that look correct, compile without errors, deploy successfully, and then silently misbehave in production.

These are the most common ones I’ve seen across real projects. Every one comes from real production incidents.

(Assumes you have cacheComponents: true enabled in next.config.ts.)

This is a follow-up to my previous post where I built a dev-only debugger to surface these issues during development. That tool helps you detect them. This post breaks down the exact failure cases behind those warnings.

Bug 1: 'use cache' on the Wrapper Instead of Inside the Function

// This looks cached. It is not.
export const getProducts = someWrapper(async function() {
  'use cache'
  cacheLife('hours')
  return db.query('SELECT * FROM products')
})

The 'use cache' directive tells the Next.js compiler to treat that function as a cache boundary. When you wrap it, the compiler sees the wrapper as the entry point. The inner function may be cached, but the wrapper becomes the execution boundary, so you still end up running it on every request.

No error. No warning. Just a function running on every request when it should be cached.

Fix:

async function _getProducts() {
  'use cache'
  cacheLife('hours')
  cacheTag('products')
  return db.query('SELECT * FROM products')
}

// Wrapper receives the already-cached function
export const getProducts = someWrapper(_getProducts)

The rule is simple: 'use cache' lives inside the data function, never on anything that wraps it.

Bug 2: Deprecated revalidateTag That Compiles and Uses Legacy Behavior

// Next.js 15: correct
// Next.js 16: TypeScript error, silently uses legacy behavior in loose tsconfig
revalidateTag('products')

In Next.js 16, revalidateTag without a second argument is deprecated and produces a TypeScript error. But if your tsconfig is not in strict mode (common in older projects), it compiles cleanly and falls back to legacy invalidation behavior instead of the new SWR-based system.

Pages stop reflecting mutations. No error anywhere.

Fix:

revalidateTag('products', 'max')         // SWR, recommended for most content
revalidateTag('products', { expire: 0 }) // Immediate expiry for webhooks/payments

Run npx @next/codemod@canary upgrade latest during your migration, it handles this automatically. But check your tsconfig strictness regardless.

Bug 3: Tag String Mismatch Across Files

// lib/data.ts -- written by developer A
async function getProducts() {
  'use cache'
  cacheTag('product-list')  // Tag is 'product-list'
  return db.query('...')
}

// app/actions/products.ts -- written by developer B
export async function createProduct(data: ProductData) {
  await db.query('INSERT INTO products ...', [...])
  revalidateTag('products', 'max')  // Different string, invalidates nothing
}

Two different strings. Zero errors. The product list never refreshes after a new product is created. Users see stale data until cacheLife expires on its own.

Fix:

// lib/tags.ts -- one source of truth
export const tags = {
  products: 'product-list',
  product: (id: string) => `product-${id}`,
  user: (id: string) => `user-${id}`,
} as const

// Both files import from tags
cacheTag(tags.products)
revalidateTag(tags.products, 'max')

Tag typos become TypeScript errors. The string mismatch bug becomes structurally impossible.

Bug 4: Server Action Mutation Where the Acting User Sees Stale Data

'use server'
export async function updateProductPrice(id: string, newPrice: number) {
  await db.query('UPDATE products SET price = $1 WHERE id = $2', [newPrice, id])
  revalidateTag(`product-${id}`, 'max')
}

revalidateTag with any named profile uses stale-while-revalidate. It marks the cache as stale. The next request still gets the cached version while fresh data loads in the background.

For the admin who just clicked save, that means they navigate to the product page and see the old price. Looks like the save failed. Causes confusion and duplicate mutations.

Fix:

'use server'
import { revalidateTag, updateTag } from 'next/cache'

export async function updateProductPrice(id: string, newPrice: number) {
  await db.query('UPDATE products SET price = $1 WHERE id = $2', [newPrice, id])
  updateTag(`product-${id}`)            // Acting user sees change immediately
  revalidateTag(`product-${id}`, 'max') // Everyone else gets SWR
  revalidateTag('products', 'max')      // Product list also refreshes
}

updateTag expires the cache entry immediately. The next request waits for fresh data. The admin sees their change. Everyone else gets the fast SWR treatment.

Constraint: updateTag only works inside Server Actions. In Route Handlers, use revalidateTag(tag, { expire: 0 }) instead.

Bug 5: updateTag in a Route Handler

// app/api/webhooks/stripe/route.ts
import { updateTag } from 'next/cache'

export async function POST(req: Request) {
  const event = await parseStripeWebhook(req)
  if (event.type === 'price.updated') {
    updateTag('products')  // Throws at runtime
  }
  return new Response('ok', { status: 200 })
}

This compiles. It deploys. On the first real webhook call from Stripe, it throws at runtime. updateTag only works inside Server Actions. Calling it anywhere else throws.

Fix:

import { revalidateTag } from 'next/cache'

export async function POST(req: Request) {
  const event = await parseStripeWebhook(req)
  if (event.type === 'price.updated') {
    revalidateTag('products', { expire: 0 })  // Immediate expiry in Route Handlers
  }
  return new Response('ok', { status: 200 })
}

Bug 6: Short cacheLife That Silently Affects PPR

async function LiveStockStatus({ productId }: { productId: string }) {
  'use cache'
  cacheLife('seconds')  // Seems right for live stock data
  cacheTag(`stock-${productId}`)
  return fetchStockLevel(productId)
}

cacheLife('seconds'), revalidate: 0, and expire under 5 minutes are automatically excluded from the PPR static shell. They become dynamic holes that run at request time.

One component with cacheLife('seconds') can push parts of the page out of the static shell and turn them into request-time work. No warning. The page still works. It just becomes fully dynamic without any obvious signal.

Fix, if the data can tolerate a short delay:

cacheLife('minutes')  // Now included in the static shell

Fix, if it genuinely needs to be live:

// Parent page
<Suspense fallback={<StockSkeleton />}>
  <LiveStockStatus productId={id} />  {/* Streams in after static shell */}
</Suspense>

The second approach is the correct PPR pattern for truly dynamic data. The static shell renders instantly and the live data streams in after.

Bug 7: Runtime API Inside a Cached Scope

async function UserHeader() {
  'use cache'
  const cookieStore = await cookies()  // Throws at build time
  const user = await getUser(cookieStore.get('user-id')?.value)
  return <div>{user.name}</div>
}

cookies(), headers(), and draftMode() are runtime APIs. They read request-specific data. They cannot live inside a 'use cache' scope because cached output is stored and replayed across requests.

This one at least throws at build time with “Uncached data was accessed outside of Suspense”. But the error gives you no component name, no file path, and no useful stack trace. You get to play binary search across your codebase to find it.

Fix:

// Read runtime values OUTSIDE the cached scope
async function UserHeader() {
  const cookieStore = await cookies()
  const userId = cookieStore.get('user-id')?.value
  return <CachedUserProfile userId={userId} />
}

// Pass the VALUE as a serializable prop to the cached component
async function CachedUserProfile({ userId }: { userId?: string }) {
  'use cache'
  cacheLife('hours')
  cacheTag(`user-${userId}`)
  if (!userId) return <GuestGreeting />
  const user = await getUser(userId)
  return <div>{user.name}</div>
}

userId is a string so it becomes part of the cache key automatically. Different users produce different cache entries without any manual key construction.

The Common Thread

None of these have good error messages. Five of the seven compile and deploy without complaint. The other two throw, but either without enough information to find the cause quickly or only after the first real production request.

The pattern across all of them is the same: the new caching model requires explicit correctness. When you get something wrong, it does not always tell you.

If you are in the middle of a Next.js 16 migration and want to catch these during development rather than in production, I ended up building a free dev-only debugger that logs cache misses, dynamic holes, missing tags, and deprecated invalidation calls directly in your terminal. Zero production cost, one .tsx file: Next.js cache debugger.

And if you want these patterns enforced at the type level so the wrong call is a compile error rather than a runtime surprise, the production enforcement layer is Cache Pro Kit.

Have you run into any of these? Or something even stranger? I’m curious what the distribution looks like across different kinds of projects.

I got tired of writing READMEs so I built a tool that generates them from your GitHub URL

Every time I finish a project the last thing I want to do is write a README. So I built Readify. Paste a GitHub URL, it scans your repo (stack, dependencies, file structure) and generates a clean professional README in seconds. Badges, install steps, features, all of it. Just copy and paste into your repo.
Built it with Next.js, Supabase and the Anthropic API. Free to try, no signup needed for the first few generations.
Would love some feedback! What sections are missing, what could be better? Link: https://readify-virid.vercel.app

FrontGate: a Lightweight Package Proxy for Supply Chain Security

Supply chain attacks are starting to feel like part of the daily routine.

You grab your morning coffee, open the laptop, and check which package ecosystem is on fire today.

Malicious packages, compromised maintainers, typosquatting, dependency confusion, suspicious new releases — the public package ecosystem is powerful, but we also trust it a lot by default.

There are mature tools for this. JFrog, Sonatype, Synk, and similar platforms exist for a reason. But not every small team, startup is ready to buy and operate a full artifact-management platform.

So I started thinking about the gap between:

“we do nothing”

and

“we adopt a full enterprise solution.”

The idea was simple:

What if package installs had a lightweight policy checkpoint?

Kind of like Cloudflare, but for dependencies.

Your existing tools still work mostly the same:

pip install ...
uv add ...
poetry add ...

But instead of talking directly to PyPI, they go through a small proxy first.

That proxy can decide whether to allow, block, warn, or log a package request based on policy. For example:

  • block packages released in the last 7 days;
  • block yanked versions;
  • block certain licenses;
  • block known-bad publishers or package names;
  • log every decision for audit/debugging.

That became FrontGate.

FrontGate is a small Go-based POC that sits between Python package clients and PyPI. It implements a PEP 503-compatible proxy surface, supports policy config via YAML, exposes Prometheus metrics, and has integration examples for pip, Poetry, uv, and CI.

It is intentionally not a private registry.

It does not try to store artifacts.

It is just a policy layer before dependencies reach your resolver.

The first use case I wanted to explore is release-age gating: blocking packages that are too new. It is not a silver bullet, but it can reduce exposure to freshly published malicious versions before the ecosystem has time to react.

But I think the idea is worth testing:

Can we make dependency installation a little less blind without introducing a heavy platform?

Repo: https://github.com/maxkrivich/frontgate

Curious if this is a problem other people have run into, especially in small teams or platform/security work where “buy the full thing” is not always the first option.

Why Your Expense Tracking Architecture Keeps Breaking

DigitXBooks Expense screenshot in English

Most developers treat expense tracking as a simple CRUD operation—a user uploads a receipt, adds a dollar amount, and hits save. Then, reality hits in the form of month-end reconciliation, missing audit trails, and the inevitable “why is this category wrong?” Slack message from the finance team.

After building and maintaining business-facing accounting tools, I’ve learned that the complexity isn’t in the database schema; it’s in the messy, high-friction handoff between the point of purchase and the general ledger. When we look at standard expense management workflows, we often ignore the operational debt that accumulates when a system doesn’t enforce context at the moment of capture.

Workflow screenshots

These screenshots are useful here because they hint at where Expense either stays grounded in the user’s real task or becomes another detached admin screen.

DigitXBooks Expense screenshot in English

The Anatomy of Expense Friction

When we build or integrate expense systems, we tend to design for the “happy path”: the user remembers the expense, categorizes it correctly, and attaches a high-resolution receipt. In production, that path is the exception. The reality is usually a mobile upload from a shaky train, a missing receipt, or a misclassified transaction that breaks the tax-readiness of the entire ledger.

1. The Context Gap

Software engineers often underestimate the importance of metadata. If an expense is just a dollar amount and a date, it’s useless to an accountant. To make an expense actionable, you need the “why.” Was it travel? Software subscriptions? Client meals? Without strict validation or intelligent suggestion at the point of entry, the system becomes a repository of “Miscellaneous” line items that eventually require hours of manual cleanup.

2. The Approval Bottleneck

If your architecture treats approvals as a simple boolean flag in a database, you’re missing the audit trail. A robust system needs to track state transitions—submitted, pending, flagged, approved, or rejected—alongside the user who initiated the change. When these states aren’t clearly defined in the backend, you end up with “zombie expenses” that sit in limbo because the workflow didn’t force a clear handoff between the requester and the approver.

Engineering Better Financial Workflows

If you are building or integrating accounting features, stop thinking about data storage and start thinking about data integrity. Here are three architectural principles that save teams from financial headaches:

  • Enforce Immutability for Audit Trails: Once an expense is submitted and verified, it should be locked. If an edit is required, generate a new version record rather than overwriting the original. This is non-negotiable for compliance.
  • Asynchronous Processing for AI Enrichment: Don’t make the user wait for OCR or category suggestions. Handle receipt scanning as a background job, updating the UI via WebSockets or polling once the enrichment is complete. This keeps the UX snappy while ensuring the data is actually useful.
  • The “Reconciliation First” Mindset: Build your database models to map directly to ledger accounts. If your expense module doesn’t speak the same language as your general ledger, you are just creating another silo that will eventually need to be reconciled by hand.

The Hidden Cost of “Easy” Features

There is a trend in modern SaaS to abstract away the “boring” parts of accounting to keep the UI clean. While this helps with user adoption, it often hides the operational reality that accounting software isn’t just a digital notebook—it’s a data integration pipeline.

When we look at platforms like DigitXBooks, the goal shouldn’t be to make expense entry “invisible,” but to make it “precise.” Providing the user with immediate feedback on why a receipt might be rejected or why a category selection is mandatory reduces the back-and-forth friction that kills productivity. Engineering for transparency—rather than just simplicity—is what separates a hobby project from a production-grade tool.

Practical Lessons for Builders

If you’re currently designing a financial workflow, do yourself a favor and map out the “negative paths” first. What happens if the receipt is blurry? What happens if the user exceeds their monthly budget? What happens when an expense is rejected three weeks after it was incurred?

If your code handles these edge cases gracefully, your users won’t just have a functional app; they’ll have a reliable system that actually saves them time during the month-end crunch. Remember, the goal of automation is to remove the manual toil, not to move the manual work from the user to the accountant.

Disclosure: This article was drafted with AI assistance from product screenshots, current trend cues, and strict human-written constraints for DEV Community style.

Closing thought

In products like DigitXBooks, the hard part of expense is rarely the screen itself. It is the product decision behind it: whether the workflow helps people act with confidence or pushes the real complexity into cleanup later. If you care about building calmer finance and operations software, follow along. I keep sharing the tradeoffs that only show up once real teams start using the product.

Question for builders

How are you designing expense in your own product so it stays useful in the moment without making the accounting side harder to trust?

Stop your AI trading agent from hallucinating technical analysis

Ask any LLM “is NVDA bullish here?” and it will answer with total confidence — a cup-and-handle here, a win rate there, an “expect a 4% move.” The problem: most of it is invented. Language models are fluent in the vocabulary of technical analysis but have no grounded source for the numbers. They’ll give you a win rate they made up.

If you’re building an agent that touches markets, that’s a real liability. The fix isn’t a better prompt — it’s a grounded tool the agent can call instead of guessing.

The idea: a base-rate engine, exposed over MCP

Chart Library is an MCP server that answers one question with real history: “given a setup that looks like this, what did statistically similar setups do next?” It returns the cohort of ~40–50 historical analogs across 10 years and 19K+ symbols, plus the forward-return distribution (median, win rate, percentile bands) — every number a verified historical fact, not a generation.

Because it speaks the Model Context Protocol, any MCP-aware client — Claude Desktop, Cursor, Cline, LangChain, the OpenAI Agents SDK — can use it in a couple of lines.

30 seconds to grounded answers

pip install chartlibrary-mcp

LangChain / LangGraph:

from langchain_mcp_adapters.client import MultiServerMCPClient
from langgraph.prebuilt import create_react_agent
from langchain_anthropic import ChatAnthropic
import os, asyncio

async def main():
    client = MultiServerMCPClient({
        "chartlibrary": {
            "command": "chartlibrary-mcp",
            "transport": "stdio",
            "env": {"CHART_LIBRARY_API_KEY": os.environ["CHART_LIBRARY_API_KEY"]},
        }
    })
    agent = create_react_agent(ChatAnthropic(model="claude-sonnet-4-6"),
                               await client.get_tools())
    out = await agent.ainvoke({"messages": [{"role": "user",
        "content": "What did setups like NVDA on 2024-08-05 do next? Cite the 5-day distribution."}]})
    print(out["messages"][-1].content)

asyncio.run(main())

The agent calls the cohort tool, gets the real distribution, and answers from it. No hallucinated stats.

The honest part (which is the whole point)

Here’s something most “AI alpha” tools won’t tell you, validated on 300K+ historical observations: chart shape predicts the size of the next move far better than the direction. Conditioning on a “bullish-looking” cohort earns a real magnitude edge (bigger average moves) but no win-rate edge — direction stays a coin flip. Our backtest shows it cleanly: a “cohort bullish” rule prints +0.77% avg vs +0.34% baseline, but 52% wins vs 54% baseline.

That’s not a bug — it’s the truth, and it’s why grounding matters. A tool that returns calibrated base rates (and tells you when it doesn’t know) is more useful to an agent than one that confidently predicts a direction it can’t.

What the agent gets

Eight read-only tools (all annotated readOnlyHint): search, cohort (the distribution primitive), discover, analyze, context, narrative, explain, portfolio. Free Sandbox tier (200 calls/day, no auth) covers search + context + explain; the full cohort depth is a paid tier.

Try it

If you’re building anything that reasons about markets, give your agent a source of truth instead of a confident guess.