Redirect Logo
Dashboard
Next.js
React
Frontend Development
frontend

Next.js 16 Upgrade: A Developer's Journey Through Chaos and Caching

Dishant Sharma
Dishant Sharma
Nov 16th, 2025
8 min read
Next.js 16 Upgrade: A Developer's Journey Through Chaos and Caching

i spent friday afternoon upgrading our e-commerce app to nextjs 16. thought it would be simple. update the package.json, run npm install, maybe fix one or two deprecations.

i was wrong. dead wrong.

my terminal looked like a crime scene. red errors everywhere. every api route in our codebase was broken. the error message didn't help: "params must be awaited." i'd seen the warnings in the beta docs. but this was stable release. shouldn't be this hard.

my product manager kept pinging me. "how's the upgrade going?" i didn't answer. couldn't admit i'd broken our entire checkout flow.

this is what they don't tell you. the breaking changes in nextjs 16 aren't just breaking. they're foundational. they touch everything.

The Setup That Failed

our app uses dynamic routes for products. /products/[id]/page.tsx. simple stuff. worked fine in nextjs 15. but now params is a promise. every single page that touches a dynamic route needs to be async. every layout. every component that uses searchparams.

i spent three hours fixing just the product pages. three hours for a one-line change per file.


I used to think caching in nextjs was magic. you wrote a page, it cached automatically. sometimes that was good. sometimes it served stale data for hours and you had no idea why.

cache components change everything. they make caching explicit. you add "use cache" to a function or component. that's it. the compiler handles the rest.

here's what i mean. i had a product recommendation component. it called our ai service. expensive. slow. i slapped "use cache" on it. added a tag in the config. now it caches for an hour.

but the first time i tried this, nothing happened. the docs say "opt-in." what they mean is "everything is dynamic by default now." you have to explicitly cache. my page was still slow. i'd forgotten to add the cachelife profile.

What Actually Happens

the compiler generates a cache key. it uses the function name, arguments, any imported variables. automatic. magical. but also confusing.

i renamed my function. the cache broke. key changed. took me twenty minutes to figure that out. the docs mention this, but it's buried. they focus on the happy path.

most tutorials tell you to just add "use cache" and you're done. they don't mention the gotchas. they don't mention that you need to think about your cache keys. that you need to understand what the compiler is doing.

here's a question people always ask: "should i cache everything?" no. absolutely not. i tried that. our memory usage spiked. cache components are powerful. but they're not free.


The first time i tried turbopack in production, our build pipeline caught fire. not literally. but it felt like it.

our builds went from 8 minutes to 3 minutes. great. but then the deployment failed. some edge case with our custom babel config. turbopack doesn't use babel by default. but we needed it for our design system.

turns out, turbopack now auto-enables babel if it finds a config. that was in the release notes. i missed it. i'd been conditioned to think "turbopack = no babel." old habits die hard.

my coworker tried to convince me to stick with webpack. "it's stable," he said. "we know it works." he's not wrong. you can still use webpack. next build --webpack. but the default is turbopack now.

i spent a day migrating our webpack-specific hacks to turbopack's model. it works. it's faster. but the learning curve is real. the error messages are better now. they actually tell you what's wrong. but you have to read them.

Why The Cache Matters

turbopack also has filesystem caching in beta. enabled it on monday. our next dev startup time dropped from 45 seconds to 12. that's not a typo.

but here's the catch. it uses disk space. a lot of it. our node_modules cache grew by 2gb. on my macbook with 256gb storage, that hurts. i had to clear old caches manually. there's no automatic cleanup yet.


Most tutorials tell you that middleware.ts is for auth, redirects, headers. that's true. but nextjs 16 renames it to proxy.ts. same functionality. different name.

the docs say this makes the "network boundary explicit." what they mean is: this runs before your app. every time. it's a proxy. not middleware.

our auth flow broke during the upgrade. we had middleware.ts checking sessions. i renamed it to proxy.ts. changed export function middleware to export function proxy. thought i was done.

but proxy.ts only runs on node.js runtime now. no more edge runtime. we were using edge for speed. had to rewrite our session checking to work on node. took another two hours.

What Broke For Us

here's what broke:

  • our edge-optimized jwt validation had to move to node

  • the request object changed slightly. headers access is async now

  • we had to add await to everything

the problem isn't what you think. it's not the rename. it's the runtime change. they deprecated edge in proxy.ts. you can still use middleware.ts for edge, but it's going away. i should have read the migration guide more carefully.

look, the new name makes sense. it's clearer. but the runtime change is the real story. that's what broke our app. not the filename.


The problem with the new caching apis isn't complexity. it's choice paralysis.

nextjs 16 gives you three ways to invalidate cache: revalidateTag(), updateTag(), and refresh(). they sound similar. they do different things.

revalidateTag() now requires a cachelife profile. you can't just call revalidateTag('products') anymore. you need revalidateTag('products', 'max'). the 'max' part enables stale-while-revalidate. users see cached data while fresh data loads in background.

i spent an hour debugging why my revalidation wasn't working. the error message was clear. i just didn't read it. i was following old patterns.

updateTag() is for server actions only. it gives you read-your-writes semantics. user updates their profile? call updateTag('user'). they see the change immediately. no stale data.

refresh() is also for server actions. but it only refreshes uncached data. doesn't touch the cache at all. good for refreshing a live notification count without invalidating your entire page.

Three Cache APIs, One Headache

which one should you use? depends. i hate that answer.

our checkout flow uses updateTag(). product listings use revalidateTag() with 'max'. admin dashboard uses refresh() for realtime stats.

the confusion is real. i had to draw a flowchart. i hate flowcharts. but i needed it. the docs explain each api. but they don't give you a decision tree.

the problem isn't the apis themselves. they're good. the problem is migrating from the old mental model. implicit caching was simple. explicit caching is powerful. but it requires you to think.


Why My Dog Hates Nextjs 16

i was up until 2am on friday. my dog max kept whining. he wanted a walk. i kept saying "five more minutes." that was a lie.

at 1am, i finally got our product pages working. async params everywhere. cache components configured. turbopack building successfully. i was ready to celebrate.

then max knocked over my coffee. right on my keyboard. the coffee, not the dog. macbook made a sad noise. screen went black.

i spent the next hour blow-drying my keyboard with a hair dryer. praying. cursing nextjs. cursing myself for not using a coffee cup with a lid.

max slept through the whole thing. didn't care. just wanted his walk. i gave him one at 3am. we both looked ridiculous.

but here's the thing. the upgrade was worth it. our load times are better. caching is more predictable. builds are faster. my dog still hates me. but customers are happier.


Real Talk: Don't Upgrade Yet

most people don't need nextjs 16 right now. if your app is on nextjs 15 and working fine? stay there. wait for 16.1.

the breaking changes are aggressive. everything is async now. that's good for performance. but it's annoying to migrate. you'll spend days adding await to every params, searchparams, cookies, headers call.

if you're on nextjs 14 or older? upgrade to 15 first. test everything. then look at 16.

small sites shouldn't bother. the performance gains are real. but they're marginal for simple apps. you won't notice. you'll just be frustrated.

this is overkill for static sites. if you're building a marketing site with 5 pages, stick to 15. or even 14. the new caching model will confuse you. you don't need it.

Who Should Upgrade

big apps with dynamic data. apps that need fine-grained caching control. teams that can spend a week on migration. that's it.

if you're starting a new project? absolutely use nextjs 16. it defaults to the right things. turbopack is great. explicit caching is the correct model. but migrating is painful.

i wouldn't do this upgrade again voluntarily. not yet. wait for the ecosystem to catch up. wait for more migration guides. save yourself the headache.


i finally got to bed at 4am. max snored next to me. my macbook keyboard was sticky but functional. the build passed. all tests green.

deployed on saturday morning. our average page load dropped from 1.2 seconds to 0.8 seconds. caching worked as expected. no more stale product data.

was it worth losing a night of sleep? probably. but i should have read the migration guide first. should have tested on a branch. should have walked my dog.

next time, i'll wait for the point release. let other people find the bugs. but for now, our app is faster. my keyboard is cleaner. and max finally got his walk.

he still ignores me when i code. smart dog.

Enjoyed this article? Check out more posts.

View All Posts