Next.js Discord

Discord Forum

Next.js Cache

Answered
Chinese Alligator posted this in #help-forum
Open in Discord
Chinese AlligatorOP
Hello everyone.
I am now working on Next.js project and I want to use cache.
And I want to clear the cache whenever I click "refresh" button to get refetched data.

Here is my script.
import { NextRequest, NextResponse } from "next/server";

const BACKEND_URL = process.env.NEXT_PUBLIC_SERVER_URL!;
const ENDPOINT = "/utils/cache-example";

export async function GET(req: NextRequest) {
    try {
        const url = new URL(req.url);
        const params = url.searchParams;

        const backendParams = new URLSearchParams();

        params.forEach((value, key) => {
            backendParams.set(key, value);
        });

        const backendUrl = `${BACKEND_URL}${ENDPOINT}?${backendParams.toString()}`;
        const res = await fetch(backendUrl, {
            headers: { "Content-Type": "application/json", Authorization: req.headers.get("Authorization")! },
            next: { revalidate: 3600, tags: ["cache-example"] },
        });

        const data = await res.json();

        return res.ok
            ? NextResponse.json(data, {
                headers: {
                    "Cache-Control": "public, s-maxage=3600, stale-while-revalidate=60",
                    "Vary": "Authorization",
                },
            })
            : NextResponse.json(
                { error: data?.error || res.statusText },
                { status: res.status }
            );
    } catch (err: any) {
        return NextResponse.json(
            { error: "Backend request failed", details: err.message },
            { status: 500 }
        );
    }
}


And here is clearing cache script.
import { NextResponse } from "next/server";
import { revalidateTag } from "next/cache";

export async function POST() {
  try {
    const tags = [
      "cache-example",
      "cache-data",
    ];

    tags.forEach(tag => revalidateTag(tag));

    return NextResponse.json({
      success: true,
      message: "Cache cleared",
      tags,
    });
  } catch (err: any) {
    return NextResponse.json(
      {
        success: false,
        error: err?.message || "Something went wrong",
      },
      { status: 500 }
    );
  }
}


The problem is that cache is not cleared and I am continuously getting the old data even though I tried to call the clearing cache api.
What is the reason and how can I clear that cache?
Thank you for your helping in advance.
Answered by Transvaal lion
This is what I came up with.

import { Posts } from '@/components/posts';

export default function Home() {
  return <Posts />;
}


'use client';

import { useEffect, useState } from 'react';
import { getData, refreshData } from './refreshData';

export function Posts() {
  const [list, setList] = useState([]);

  async function handleClick() {
    await refreshData();

    const searchParams = new URLSearchParams(window.location.search);
    const data = await getData(searchParams.get('sort'));
    setList(data);
  }

  useEffect(() => {
    (async () => {
      const searchParams = new URLSearchParams(window.location.search);
      const data = await getData(searchParams.get('sort'));
      setList(data);
    })();
  }, []);

  return (
    <section>
      <button onClick={handleClick} className="bg-red-500 p-2">
        Refresh
      </button>
      <ul>
        {list.map((x) => (
          <li key={x}>{x}</li>
        ))}
      </ul>
    </section>
  );
}


'use server';

import { revalidateTag } from 'next/cache';

export async function refreshData() {
  revalidateTag('next-cache', 'max');
}

export async function getData(sort: string | null) {
  const res = await fetch('http://localhost:3000/api/get-posts', {
    headers: {
      'x-sort': sort === 'desc' ? 'desc' : 'asc',
    },
    cache: 'force-cache',
    next: { revalidate: 3600, tags: ['next-cache'] },
  });
  const data = await res.json();

  return data;
}


import { NextRequest, NextResponse } from 'next/server';

export async function GET(req: NextRequest) {
  const headers = req.headers;

  const sort = headers.get('x-sort');
  const res = await fetch('http://localhost:3001/posts', {
    cache: 'force-cache',
    next: { revalidate: 3600, tags: ['next-cache'] },
  });
  const data = (await res.json()) as string[];

  return NextResponse.json(sort === 'desc' ? [...data].reverse() : data);
}
View full answer

28 Replies

Transvaal lion
Disclaimer: I am not expert in this topic, still trying to contribute

TL;DR: use updateTag in server action

AFAIK revalidateTag revalidates the data from data-cache only, your client-cache still is there in the browser and for that reason stale data will be displayed until you refresh the page or hit the GET route after hitting the revalidate route in the handleClick of refresh button.

So my best bet is that, instead of calling POST route handler, you call server action in handleClick and call updateTag there. This will purge the client cache and server cache both and also give the fresh data synchronously.
@Transvaal lion Disclaimer: I am not expert in this topic, still trying to contribute TL;DR: use updateTag in server action AFAIK revalidateTag revalidates the data from data-cache only, your client-cache still is there in the browser and for that reason stale data will be displayed until you refresh the page or hit the GET route after hitting the revalidate route in the handleClick of refresh button. So my best bet is that, instead of calling POST route handler, you call server action in handleClick and call updateTag there. This will purge the client cache and server cache both and also give the fresh data synchronously.
Chinese AlligatorOP
Hi, @Transvaal lion
Thank you for your answering.
Can you check this script, please?
const refreshData = async () => {
    try {
      setIsLoading(true);
      // await clearCache();
      await refreshData();
      await fetchFilteredData();
    } catch (error) {
      setIsLoading(false);
    }
  };


Here, fetchFilteredDatais the function to get the data from api.
In this function, I call the server API like this and set state using the response.

export async function fetchKpiEngineers() {
  try {
    const token = getToken();

    const query = new URLSearchParams({
      optionId: "Open",
    }).toString();

    const res = await fetch(`/api/utils/cache-example?${query}`, {
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${token}`,
      },
    });
    if (!res.ok) throw new Error("Failed to fetch data");
    return await res.json();
  } catch (err) {
    console.error(err);
    return [];
  }
}

And refreshData is called when I click refresh button.
clearCache is the api already shared and refreshData is server action what you mentioned.
"use server";

import { revalidateTag } from "next/cache";

export async function refreshData() {
  revalidateTag("kpi-engineer");
}


Even though, I use server action or api, none of them are not clearing the cache.
You are mixing two different cache layers:

Next.js Data Cache from this line:

next: { revalidate: 3600, tags: ["cache-example"] }


HTTP/CDN/browser response cache from this line:

"Cache-Control": "public, s-maxage=3600, stale-while-revalidate=60"


revalidateTag("cache-example") only invalidates the Next.js tagged Data Cache

It doesn't clear a CDN/shared HTTP cache that was created by Cache-Control: public, s-maxage=3600
Also, because your response depends on Authorization, you probably should not use public cache for this response. Even with Vary: Authorization, it is safer to avoid public shared caching for authenticated API responses
Transvaal lion
I have tried to reproduce your scenario to purge the client cache immediately after revalidating.

But the problem is on first click refresh button revalidates the cache from data cache and returns the stale data. Server in background refreshes the data.

On second click, server returns the fresh data.

I tried to purge the client cache after the first click itself using updateTag and router refresh, but still it is not working. I am not understanding the updateTag behaviour for client component. If you use server Component to fetch data, updateTag syncs the UI also immediately, but unfortunately client component are showing exception. Maybe someone can enlighten this behavior.

So apart from server component solution, this is working for me.

1. Remove the Cache control
2. in server action, call updateTag() and then refresh()


Hope someone can give proper solution for this
@π™²πš˜πš› You are mixing two different cache layers: Next.js Data Cache from this line: next: { revalidate: 3600, tags: ["cache-example"] } HTTP/CDN/browser response cache from this line: "Cache-Control": "public, s-maxage=3600, stale-while-revalidate=60" revalidateTag("cache-example") only invalidates the Next.js tagged Data Cache It doesn't clear a CDN/shared HTTP cache that was created by Cache-Control: public, s-maxage=3600
Chinese AlligatorOP
Hi @π™²πš˜πš› , thank you for your answering.
So your suggestion is to remove "Cache-Control": "public, s-maxage=3600, stale-while-revalidate=60". Right?
Even though I remove it, the cache is not cleared and continuously returning the old data.
I already tried this way. Is there any other way?
Transvaal lion
"use server";

import { updateTag, refresh} from "next/cache";

export async function refreshData() {
  updateTag("kpi-engineer");
  refresh()
}


I didn't try but you can also try and check revalidateTag in server action + router.refresh in handleClick combo
Transvaal lion
Oh. I was checking in 16. Your?
@Transvaal lion Oh. I was checking in 16. Your?
Chinese AlligatorOP
Oh, it was 14
@Transvaal lion Oh. I was checking in 16. Your?
Chinese AlligatorOP
I will update the version and try
Transvaal lion
Ok. I will also try to check in 14
Chinese AlligatorOP
@Transvaal lion
So does this work on your side?
I updated the next version to 16.2.4
import { NextRequest, NextResponse } from "next/server";

const BACKEND_URL = process.env.NEXT_PUBLIC_SERVER_URL!;
const ENDPOINT = "/utils/cache-example";

export async function GET(req: NextRequest) {
    try {
        const url = new URL(req.url);
        const params = url.searchParams;

        const backendParams = new URLSearchParams();

        params.forEach((value, key) => {
            backendParams.set(key, value);
        });

        const backendUrl = `${BACKEND_URL}${ENDPOINT}?${backendParams.toString()}`;
        const res = await fetch(backendUrl, {
            headers: { "Content-Type": "application/json", Authorization: req.headers.get("Authorization")! },
            next: { revalidate: 3600, tags: ["cache-example"] },
        });

        const data = await res.json();

        return res.ok
            ? NextResponse.json(data, {
                headers: {
                    // "Cache-Control": "public, s-maxage=3600, stale-while-revalidate=60",
                    "Vary": "Authorization",
                },
            })
            : NextResponse.json(
                { error: data?.error || res.statusText },
                { status: res.status }
            );
    } catch (err: any) {
        return NextResponse.json(
            { error: "Backend request failed", details: err.message },
            { status: 500 }
        );
    }
}


"use server";

import { updateTag, refresh} from "next/cache";

export async function refreshData() {
  updateTag("cache-example");
  refresh()
}



const refreshKPI = async () => {
    try {
      setIsLoading(true);
      await refreshData();
      await fetchFilteredData();
    } catch (error) {
      setIsLoading(false);
    }
  };
Transvaal lion
Yes, but i tested simple dummy backend with no search params, it was working
@Transvaal lion Yes, but i tested simple dummy backend with no search params, it was working
Chinese AlligatorOP
Is it client component?
Transvaal lion
Yes
@Transvaal lion Yes
Chinese AlligatorOP
I tried with above scripts, but it doesn't work in my side.
What I did is
Remove Cache-Control
Use updateTag and refresh in server action.

But still doesn't work.
Transvaal lion
Is it possible to fetch the data in the server component?
@Transvaal lion Is it possible to fetch the data in the server component?
Chinese AlligatorOP
The Get request's search paramters are rely on localStorage.
Chinese AlligatorOP
export async function fetchData() {
  try {
    const token = getToken();

    const query = new URLSearchParams({
      optionId: "Open",
    }).toString();

    const res = await fetch(`/api/utils/cache-example?${query}`, {
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${token}`,
      },
    });
    if (!res.ok) throw new Error("Failed to fetch data");
    return await res.json();
  } catch (err) {
    console.error(err);
    return [];
  }
}


This is the function to get fresh data by calling api endpoints.
Here, optionId is rely on localStorage value and there are more other search parameters.
in fetchFilteredData function, I call fetchData function and set the state values with the response.
Transvaal lion
This is what I came up with.

import { Posts } from '@/components/posts';

export default function Home() {
  return <Posts />;
}


'use client';

import { useEffect, useState } from 'react';
import { getData, refreshData } from './refreshData';

export function Posts() {
  const [list, setList] = useState([]);

  async function handleClick() {
    await refreshData();

    const searchParams = new URLSearchParams(window.location.search);
    const data = await getData(searchParams.get('sort'));
    setList(data);
  }

  useEffect(() => {
    (async () => {
      const searchParams = new URLSearchParams(window.location.search);
      const data = await getData(searchParams.get('sort'));
      setList(data);
    })();
  }, []);

  return (
    <section>
      <button onClick={handleClick} className="bg-red-500 p-2">
        Refresh
      </button>
      <ul>
        {list.map((x) => (
          <li key={x}>{x}</li>
        ))}
      </ul>
    </section>
  );
}


'use server';

import { revalidateTag } from 'next/cache';

export async function refreshData() {
  revalidateTag('next-cache', 'max');
}

export async function getData(sort: string | null) {
  const res = await fetch('http://localhost:3000/api/get-posts', {
    headers: {
      'x-sort': sort === 'desc' ? 'desc' : 'asc',
    },
    cache: 'force-cache',
    next: { revalidate: 3600, tags: ['next-cache'] },
  });
  const data = await res.json();

  return data;
}


import { NextRequest, NextResponse } from 'next/server';

export async function GET(req: NextRequest) {
  const headers = req.headers;

  const sort = headers.get('x-sort');
  const res = await fetch('http://localhost:3001/posts', {
    cache: 'force-cache',
    next: { revalidate: 3600, tags: ['next-cache'] },
  });
  const data = (await res.json()) as string[];

  return NextResponse.json(sort === 'desc' ? [...data].reverse() : data);
}
Answer
Transvaal lion
This is the dummy express backend.
const express = require('express');
const cors = require('cors');
const app = express();
const port = 3001;

app.use(cors());

app.get('/posts', (req, res) => {
  console.log('Request came: ', Date.now().toLocaleString());
  res.json(['post-1', 'post-2']);
});

app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});
@Transvaal lion This is what I came up with. tyepscript import { Posts } from '@/components/posts'; export default function Home() { return <Posts />; } typescript 'use client'; import { useEffect, useState } from 'react'; import { getData, refreshData } from './refreshData'; export function Posts() { const [list, setList] = useState([]); async function handleClick() { await refreshData(); const searchParams = new URLSearchParams(window.location.search); const data = await getData(searchParams.get('sort')); setList(data); } useEffect(() => { (async () => { const searchParams = new URLSearchParams(window.location.search); const data = await getData(searchParams.get('sort')); setList(data); })(); }, []); return ( <section> <button onClick={handleClick} className="bg-red-500 p-2"> Refresh </button> <ul> {list.map((x) => ( <li key={x}>{x}</li> ))} </ul> </section> ); } typescript 'use server'; import { revalidateTag } from 'next/cache'; export async function refreshData() { revalidateTag('next-cache', 'max'); } export async function getData(sort: string | null) { const res = await fetch('http://localhost:3000/api/get-posts', { headers: { 'x-sort': sort === 'desc' ? 'desc' : 'asc', }, cache: 'force-cache', next: { revalidate: 3600, tags: ['next-cache'] }, }); const data = await res.json(); return data; } typescript import { NextRequest, NextResponse } from 'next/server'; export async function GET(req: NextRequest) { const headers = req.headers; const sort = headers.get('x-sort'); const res = await fetch('http://localhost:3001/posts', { cache: 'force-cache', next: { revalidate: 3600, tags: ['next-cache'] }, }); const data = (await res.json()) as string[]; return NextResponse.json(sort === 'desc' ? [...data].reverse() : data); }
Chinese AlligatorOP
Thank you. I will try this way.
Chinese AlligatorOP
@Transvaal lion I tried that way but still doesn't work in my side. 😭
@Chinese Alligator Hi <@1287103956912312503> , thank you for your answering. So your suggestion is to remove `"Cache-Control": "public, s-maxage=3600, stale-while-revalidate=60"`. Right? Even though I remove it, the cache is not cleared and continuously returning the old data. I already tried this way. Is there any other way?
Yes, exactly. Removing
"Cache-Control: public, s-maxage=3600..." 
is one part of it.

But if you already removed it and still get old data, then the stale data is probably coming from another cache layer.

I would check these possibilities:

1. Next.js Data Cache
2. client-side cache, like SWR, React Query, or local state
3. browser fetch cache
4. backend/API cache
5. route/path cache, not only tag cache

I would try this first:

import { NextResponse } from "next/server";
import { revalidateTag, revalidatePath } from "next/cache";

export async function POST() {
  revalidateTag("cache-example", { expire: 0 });
  revalidatePath("/api/cache-example");

  return NextResponse.json(
    {
      success: true,
      message: "Cache cleared",
      now: Date.now(),
    },
    {
      headers: {
        "Cache-Control": "no-store",
      },
    }
  );
}

And when you call it from the refresh button, make sure the client request also bypasses cache:
await fetch("/api/clear-cache", {
  method: "POST",
  cache: "no-store",
});

const res = await fetch("/api/cache-example", {
  cache: "no-store",
  headers: {
    Authorization: token,
  },
});

Also, add a temporary log inside your "GET" route:
console.log("API route called", Date.now());

If this log does not appear after clicking refresh, then the frontend is not actually refetching the API. In that case, depending on how you fetch the data, you may need

 "router.refresh()", "mutate()", or "queryClient.invalidateQueries()".
``
If the log appears but the data is still old, then the issue is probably either the Next.js fetch cache or the backend itself is returning cached data.

One more important point: since your request depends on "Authorization", be careful with shared caching. If the response is user-specific, either avoid caching it or use a user-specific tag, for example:

tags: [cache-example-${userId}]

Otherwise different authorized users could accidentally receive the same cached response.

So the next thing I would check is whether the "GET" route is actually called again after the clear-cache API runs. That should tell us which cache layer is causing the issue.