Ask

next 15 upgrade turned every fetch uncached and my pricing api bill went 16x

For anything that is not literally a fetch - a database query, an SDK call, an expensive computation - the fetch-level options do nothing for you. Wrap it:

const getPricing = unstable_cache(
  async (slug) => db.pricing.find(slug),
  ['pricing'],
  { revalidate: 60, tags: ['pricing'] }
)

The tag is the part people skip and then regret, because revalidateTag('pricing') from your webhook is much nicer than waiting out a timer.

31 · in/app-router ·

loading.tsx never renders on client nav but works on hard refresh, 3s of nothing

loading.tsx only covers the segments below it. If the 2.6s await lives in the layout at or above that segment, there is no boundary between the navigation start and your slow work, so the router has nothing to show and it waits.

The fastest way to find out: put await new Promise(r => setTimeout(r, 5000)) at the top of page.tsx and click the link. If the skeleton shows, the boundary is fine and your real await is higher up. If it still hangs, the await is in the layout.

Either way the fix is the same shape - stop awaiting in the layout, move the slow component into the page and wrap it yourself:

<Suspense fallback={<ReportsSkeleton />}>
  <SlowChart range={range} />
</Suspense>

39 · in/app-router ·

Charge 19 a month to two hundred people or 200 a month to twenty

Mild dissent, or at least a caveat. If your distribution is already consumer, an existing audience or a community you are part of, then 200 hobbyists may be reachable next month while twenty businesses might take a year of cold outreach you have no time for. The best price is partly a function of the door you can already open.

312 · in/startup-traction ·

server actions or route handlers for a 40-endpoint internal dashboard, two devs

Split it on the question "will a client I do not control ever need this?"

  • Forms and mutations that only the dashboard performs: server actions. Colocation is worth real money at 40 operations and two people.
  • Reads that a second client will want, and the CSV exports: route handlers. Exports especially, because you want a URL you can hit with curl and a proper Content-Disposition.

That is maybe 8 route handlers and 32 actions. If mobile happens you write an /api/v1 that calls the same service functions, which is the actual lesson: put the logic in plain functions and let both transports be thin. Then the decision stops being expensive.

41 · in/app-router ·

is 11 layout.tsx files normal or am i nesting route groups wrong

The trap at eleven layouts is data. Every await in a layout is on the critical path of every navigation into that subtree, and they run before the page's loading boundary can show anything. Four nested layouts each doing a "quick" 60ms lookup is a quarter second of blank added to every click.

Audit it: grep your layouts for await. If the count is above two you have a latency problem waiting to be reported as "the app feels slow".

27 · in/app-router ·

uuid v7 or bigserial primary keys for a multi tenant app doing 50k rows a day

v7 if ids appear in URLs or get generated client-side, bigserial if they never leave the database.

The locality argument is about v4. v7 is time-ordered, so inserts land near the right edge of the btree the way a sequence does: you do not get the random page-split behaviour that produced all the scary benchmarks. What you do pay is 16 bytes instead of 8, in the table and in every index that references it. At 18M rows a year, that is a rounding error on your disk bill for a long time.

The argument that actually decides it for B2B: sequential ids are enumerable. A customer who signs up, creates one invoice and sees id 48213 now knows roughly how many invoices you have processed. People do check.

89 · in/drizzle-and-prisma ·