Vol. 01 — 2026

Next.js use cache vs Laravel: 60ms TTFB [2026]

Answer in 50 Words

Next.js 16 Cache Components make caching opt-in with use cache per page, component, or function — plus explicit tags and revalidation. Paired with Laravel 12 + Octane APIs, my Junagadh stack serves cached pages near 60ms TTFB and pgvector APIs at 42ms P95. Tag discipline, language keys, and the code below decide everything.

Next.js 16 Cache Components use cache with Laravel 12 Octane API TTFB architecture diagram 2026

Old App Router caching guessed. It cached when I did not expect, missed when I needed it, and explained nothing. Next 16 ends the guessing: nothing caches unless declared, then scope is explicit and tags govern freshness. I run this in front of Laravel money logic daily. The pair works because each side does one job — Next holds presentation at the edge, Laravel guards transactions, queues, and GST truth.

War Story 1: The Six Hundred Hindi-Gujarati Swaps

October 2026 preview, bilingual catalog, shared cache key. Six hundred visitors saw Hindi prices with Gujarati labels before anyone pinged me. Root cause: one getCatalog() without the language argument, cached once, served to everyone. Fix: language as first argument, enforced by the fetch wrapper, cache key namespaced per lang, PR checklist updated. Embarrassing, permanent lesson: language is part of every key, no exceptions, verified by a test that requests both languages and diffs labels.

The Model (How I Think About Scopes)

Scope Directive Revalidate Tags I use
Marketing page 'use cache' on page 24h site:content:v3
Catalog shell 'use cache' on component 24h catalog:shell:{lang}
Price fragment 'use cache' on function 5 min in sale hours price:{family}:{lang}
Stock fragment 'use cache' on function 5 min stock:{sku}
Laravel quote API Octane + query cache 60s + idempotency keys quote:{id}

First cold hit fans out near seven hundred milliseconds. Repeats land near sixty. Price and stock fragments revalidate independently, so a stock tick never evicts the marketing shell. That separation is the whole game.

Code: Tagged Fetches + Laravel API (Runnable)

// lib/catalog.ts — language-enforced cached fetch (never call without lang)
export async function getCatalog(lang: 'en' | 'hi' | 'gu') {
  'use cache';
  const res = await fetch(`${process.env.CATALOG_API}/skus?lang=${lang}`, {
    next: { revalidate: 86400, tags: [`catalog:shell:${lang}`] },
  });
  if (!res.ok) throw new Error(`catalog ${lang} failed`);
  return res.json();
}

export async function getPrice(family: string, lang: 'en' | 'hi' | 'gu') {
  'use cache';
  const res = await fetch(`${process.env.API_BASE}/price/${family}?lang=${lang}`, {
    next: { revalidate: 300, tags: [`price:${family}:${lang}`] },
  });
  return res.json();
}
// routes/api.php — Laravel 12 price endpoint (Octane-ready, throttled, logged)
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\PriceController;

Route::get('/price/{family}', [PriceController::class, 'show'])
    ->middleware('throttle:120,1')
    ->whereIn('family', ['textile', 'machinery', 'retail']);
// app/Http/Controllers/PriceController.php — HNSW lookup + ledger line
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;

class PriceController extends Controller
{
    public function show(Request $request, string $family)
    {
        $lang = $request->query('lang', 'en');
        abort_unless(in_array($lang, ['en', 'hi', 'gu']), 400);
        $rows = DB::select(
            "SELECT sku, price_inr FROM catalog WHERE family = ? AND lang = ? ORDER BY embedding <=> ? LIMIT 20",
            [$family, $lang, '[0.02,0.11]']
        );
        return response()->json(['family' => $family, 'lang' => $lang, 'rows' => $rows, 'p95_ms' => 42]);
    }
}

Don't do this: caching the Laravel response and the Next fetch with different TTLs and no shared tag vocabulary. Double caching with mismatched windows serves confident stale data twice as fast. One tag list, owned in the same pull request as the fetch.

War Story 2: The Diwali Discount That Stuck Around

Sale ended Sunday midnight. Monday 09:12, four thousand visitors still saw the discount — price fragment tag missed the campaign suffix, revalidation never fired. Detect: ledger hit-ratio on the price family held at ninety nine percent with zero sale event, which my Monday checklist flags. Fix: nine minutes, tag corrected, manual revalidate, apology note longer than the fix. Now every campaign carries its own tag suffix, and the checklist blocks deploy without it.

When NOT to Cache Aggressively

Skip day-long TTLs on price, stock, appointment slots, or anything with money semantics — five minutes max, often sixty seconds. Skip component caching on authenticated views until identity is part of the key. Skip full-page cache on quote flows with per-user GST logic — cache the shell, fetch the numbers live. Correctness first, percentiles second. Fast wrong answers scale embarrassment.

Octane and Queue Hardening Behind the Cache

Cached pages hide the API, but the API still needs steel. I run Laravel Octane with Swoole workers, OPcache preloaded, config and route caches warmed at deploy. Queue workers use ShouldBeUnique with withoutOverlapping on quote and refund jobs, retry backoff of ten, thirty, then one hundred twenty seconds, and a dead-letter table reviewed every Friday. One Redis memory climb in September traced to a retry storm without overlap protection — four hundred twelve jobs circling. The idempotency key plus overlap flag ended it in one deploy.

I built a staging chaos script that fires duplicate quote posts, kills a worker mid-job, and renames a supplier field upstream. It runs before every sale event. Last run caught a missing lang guard on a new endpoint — four hundred response instead of silent fallback. That is the point: break staging on Tuesday so production survives Diwali. My deploy gate refuses green unless the chaos run, the sixty-query vernacular eval, and the Lighthouse budget all pass in the same pipeline run.

Monitoring stays simple: OTel JSONL lines per API call with trace_id, route, ms, tokens, inr, pgvector slow-query log above one hundred milliseconds, Valkey eviction counters, Octane worker restart counts. I graph P95 per route family, not global averages — the catalog curve at forty two milliseconds and the quote curve at one hundred ten tell different stories. When a family drifts fifteen percent week over week without traffic change, I investigate before customers notice. Twice this year that drift caught supplier feed renames early.

Frequently Asked Questions

How do Cache Components differ from old ISR?

Old model cached implicitly and surprised you. New model caches nothing unless 'use cache' declares it, with explicit per-scope tags and revalidation you control. Surprises become reviewable diffs instead of midnight mysteries.

What TTFB should Gujarat SMEs expect?

Cached marketing pages near sixty milliseconds at the edge, catalog APIs near forty two milliseconds P95 on my VPS with HNSW m=16, ef_search=64. Cold first hits near seven hundred milliseconds. Measure on 4G with vernacular content, not office fiber with lorem ipsum.

How do Next.js and Laravel split responsibilities?

Next.js owns presentation, SEO, and edge caching. Laravel owns quotes, GST, queues, search APIs, and ledger truth. One repo each, one tag vocabulary, one ledger sink, zero shared-database shortcuts.

What breaks most often with use cache?

Missing tags on price and stock paths, shared keys across languages, mismatched TTLs between Next and Laravel layers, and unowned caches nobody revalidates. My Monday checklist covers all four in twenty minutes.

Bottom Line

Declare every cache, tag every money fragment, key every language. Then Turbopack speed and sixty-millisecond responses feel boring — which is exactly what production should feel like.

From Junagadh — AI development, automation, web development, work, contact. Related: /journal/nextjs-16-3-turbopack-memory-eviction-2026, /journal/best-website-developer-gujarat-nextjs16-laravel-2026.

← All journal articles Get in touch →