How to Fix: Next v15 Double Render/Request
Next.js 15 double render or duplicate request: why it happens and how to stop misdiagnosing it
You fetch data once, but your logs show two renders or two requests. In Next.js 15, this is usually not a broken fetch layer or a rogue component loop. Most of the time, it is the result of React Strict Mode, development-only rendering behavior, or a misunderstanding of how the App Router executes server components.
Table of Contents
Symptoms of the issue
This issue commonly appears like this:
- A page-level server component logs twice in development.
- A fetch call appears to run twice when opening a route.
- An API endpoint receives duplicate requests during local development.
- Developers assume caching is broken, even though production behaves differently.
In the reported Next.js 15 scenario, the important detail is that the behavior is often reproducible in development mode and not necessarily in a production build.
Understanding the Root Cause
The main reason this happens is that Next.js 15 runs on React development behavior that intentionally re-invokes rendering paths to help surface side effects, unsafe mutations, and non-idempotent logic.
There are usually three overlapping causes:
1. React Strict Mode in development
With Strict Mode, React may invoke render-related logic more than once during development. This is intentional. The goal is to expose code that causes side effects during render or depends on execution happening only once.
If your server component, client component, or data loader prints logs or triggers requests in a non-idempotent way, it can look like a bug even when the framework is doing exactly what it is designed to do.
2. App Router server component execution model
In the App Router, server components are part of the rendering pipeline. During development, Next.js may perform extra work for validation, hot reload support, and tree evaluation. That can make component functions and related fetch logic appear to execute more than once.
This does not always mean users in production will trigger duplicate network traffic.
3. Confusing render duplication with request duplication
A critical distinction:
- Double render means component code executes twice.
- Double request means your backend or external API is actually being hit twice.
Those are related, but not identical. If your fetch is cacheable, memoized, or deduplicated by Next.js, two render passes may still collapse into one effective data request. On the other hand, if you use cache: ‘no-store’, dynamic request headers, or custom fetch wrappers, the framework may be unable to deduplicate anything.
Why this is especially visible in Next.js 15
Next.js 15 continues the modern App Router and React server component architecture, where development ergonomics and correctness checks can make execution patterns feel different from older page-based mental models. If your code assumes a page function runs exactly once, that assumption is no longer safe in development.
Step-by-Step Solution
The fix is usually not to “stop Next from rendering twice,” but to make your code safe, cache-aware, and correctly tested.
Step 1: Verify whether the issue only happens in development
First, test a production build locally:
npm run build
npm run start
If the duplicate behavior disappears in production, you are likely seeing expected dev-only behavior, not a framework bug.
Step 2: Remove side effects from render logic
Do not place non-idempotent logic directly in component render paths.
Problematic pattern:
export default async function Page() {
console.log('render');
await fetch('https://example.com/api/log-visit', {
method: 'POST'
});
return <div>Hello</div>;
}
This is dangerous because rendering may happen more than once, especially in development.
Better pattern: only fetch data that is safe to re-run, and move mutations to explicit actions, route handlers, or user-triggered events.
export default async function Page() {
const res = await fetch('https://example.com/api/products', {
next: { revalidate: 60 }
});
const products = await res.json();
return <div>{products.length} products</div>;
}
Step 3: Use fetch caching intentionally
If your request is read-only, configure caching so Next.js can optimize and deduplicate where appropriate.
const res = await fetch('https://example.com/api/products', {
next: { revalidate: 60 }
});
Or, if the request should be static for the build/runtime boundary:
const res = await fetch('https://example.com/api/products', {
cache: 'force-cache'
});
If you explicitly use this:
const res = await fetch('https://example.com/api/products', {
cache: 'no-store'
});
then you are opting out of cache reuse, so multiple executions can become multiple real requests.
Step 4: Deduplicate custom data access with React cache when needed
If you are wrapping database or API calls in your own function, use a deduplication strategy such as React cache for repeated calls during the same render lifecycle.
import { cache } from 'react';
export const getProduct = cache(async (id: string) => {
const res = await fetch(`https://example.com/api/products/${id}`);
return res.json();
});
This is useful when the same data accessor may be called by multiple server components in the same request tree.
Step 5: Keep mutations out of GET-style page rendering
If a page load is causing writes, analytics inserts, token consumption, or webhook-like behavior, move that logic into one of these:
- Route handlers
- Server actions
- Client-side event handlers
- Background jobs
Example using a route handler:
// app/api/track/route.ts
import { NextResponse } from 'next/server';
export async function POST() {
// write to DB or analytics provider here
return NextResponse.json({ ok: true });
}
Then call it from a client interaction instead of from the page render itself.
Step 6: If needed, confirm Strict Mode behavior
If you need to isolate whether Strict Mode is responsible, temporarily inspect your Next.js configuration.
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
reactStrictMode: true,
};
export default nextConfig;
For debugging only, teams sometimes temporarily disable Strict Mode to compare behavior. If duplicate execution disappears, you have confirmed the source. However, the recommended long-term fix is still to write idempotent render logic, not to rely on Strict Mode being off.
Step 7: Distinguish logs from actual backend hits
Add server-side instrumentation so you can tell whether you are seeing:
- two console logs from two render passes, or
- two real outbound HTTP requests, or
- one request plus one cached read.
For example, inspect backend access logs or add unique request IDs around your data layer rather than trusting component-level console output alone.
export default async function Page() {
const requestId = crypto.randomUUID();
console.log('page render', requestId);
const res = await fetch('https://example.com/api/products', {
next: { revalidate: 60 }
});
console.log('fetch completed', requestId);
return <div>Done</div>;
}
This helps separate framework rendering behavior from true duplicated I/O.
Common Edge Cases
Using no-store everywhere
If every fetch is marked dynamic with cache: ‘no-store’, expect more real requests. That is correct behavior, not a caching regression.
Calling your own API route from a server component
If a server component calls an internal API route instead of directly accessing the database or service layer, you may introduce avoidable network hops and make duplicate request symptoms look worse. In App Router code, prefer shared server-side functions when possible.
Hot reload during development
Fast Refresh can retrigger component execution while editing. This can look like random duplication if you are watching logs while saving files.
Non-idempotent analytics or inserts
If a page render performs a write, duplicate development execution can create duplicate records. Protect against this with idempotency keys, unique constraints, or by moving writes out of render paths entirely.
Multiple components requesting the same resource differently
If one component fetches with force-cache and another with no-store, deduplication expectations break down. Keep request options consistent for the same resource.
Headers or cookies making the route dynamic
Using request-bound values such as headers() or cookies() can force dynamic rendering behavior. That may reduce caching opportunities and increase observable fetch executions.
FAQ
Is Next.js 15 actually sending every request twice?
No. In many cases, you are seeing development-time double rendering, not guaranteed duplicate backend traffic. Always compare development mode with a production build before concluding there is a real network duplication bug.
Should I disable Strict Mode to fix this?
Usually no. Strict Mode is helping reveal unsafe assumptions in your code. The better fix is to make render-time logic idempotent, cache read-only fetches properly, and move mutations out of the render path.
Why does my API still get hit twice even after I understand the render behavior?
Common reasons include cache: ‘no-store’, dynamic route execution, custom fetch wrappers that bypass framework optimizations, or non-shared data access logic being called from multiple places. Inspect request options and centralize your server data layer.
Recommended final approach
For this Next.js 15 issue, the most reliable solution is:
- Confirm whether the behavior is development-only.
- Keep page and server component rendering free of side effects.
- Use fetch caching and revalidation intentionally.
- Move writes and tracking to route handlers, server actions, or explicit user events.
- Use deduplication patterns like React cache for shared server reads.
Once you adopt that model, the apparent Next.js 15 double render or double request issue becomes much easier to reason about, and your code will behave correctly in both development and production.