How to Fix: Reading searchParams with “use cache” causes the build to hang

5 min read

A Next.js build that never finishes is usually a signal that request-bound data was pulled into a cached execution path. In this case, reading searchParams inside code using "use cache" creates a conflict between per-request state and static/cacheable evaluation, which can cause the build pipeline to stall instead of completing cleanly.

Understanding the Root Cause

The issue happens because searchParams is inherently request-specific. Its value depends on the incoming URL at runtime. By contrast, the "use cache" directive tells Next.js that the function or component can be treated as cacheable and evaluated in a way compatible with build-time or shared reuse.

Those two ideas are incompatible:

  • searchParams needs a live request context.
  • "use cache" expects deterministic, reusable output.

When a cached boundary tries to read request data, Next.js cannot safely determine whether the result should be static, dynamic, or memoized per input. In affected cases, that unresolved state can surface as a build hang during npm run build.

Technically, this is similar to mixing dynamic rendering signals into a static optimization boundary. The framework attempts to analyze the route during build, but reading searchParams forces runtime behavior. If that access occurs under "use cache", the caching model breaks down.

The safe rule is simple: never read request-scoped values inside a cached function or cached component boundary. Extract the value first, then pass only plain serializable inputs into cacheable logic.

Step-by-Step Solution

The fix is to separate request parsing from cacheable computation.

1. Identify the problematic pattern

A simplified version of the problematic approach looks like this:

async function getData(searchParams) {
  "use cache"

  const query = searchParams.q
  return fetchSomething(query)
}

export default async function Page({ searchParams }) {
  const data = await getData(searchParams)
  return <div>{data}</div>
}

This is unsafe because the cached function directly depends on searchParams.

2. Read searchParams outside the cached boundary

Extract the needed values in the page or route entry point first:

async function getData(query) {
  "use cache"

  return fetchSomething(query)
}

export default async function Page({ searchParams }) {
  const query = typeof searchParams.q === "string" ? searchParams.q : ""
  const data = await getData(query)

  return <div>{data}</div>
}

Now the cached function receives a plain string, not a request-bound object.

3. Keep cached functions deterministic

Your cached function should depend only on explicit inputs:

async function getFilteredPosts(query, page) {
  "use cache"

  return db.posts.findMany({
    where: { title: { contains: query } },
    take: 10,
    skip: page * 10
  })
}

Then pass validated values into it:

export default async function Page({ searchParams }) {
  const query = typeof searchParams.q === "string" ? searchParams.q : ""
  const page = Number.isFinite(Number(searchParams.page))
    ? Number(searchParams.page)
    : 0

  const posts = await getFilteredPosts(query, page)

  return <div>{posts.length}</div>
}

4. If the route must stay dynamic, do not force caching there

If the page fundamentally depends on request-time URL state and should not be cached at that boundary, remove "use cache" from the function reading URL-derived inputs:

async function getData(query) {
  return fetchSomething(query)
}

This is often the correct choice when the output must reflect every request variation immediately.

5. Move cacheable work deeper into pure helpers

A good production pattern is:

  • Page reads searchParams
  • Page validates and normalizes inputs
  • Pure helper with "use cache" receives only primitives
function normalizeQuery(value) {
  return typeof value === "string" ? value.trim() : ""
}

async function searchPosts(query) {
  "use cache"

  return fetchSomething(query)
}

export default async function Page({ searchParams }) {
  const query = normalizeQuery(searchParams.q)
  const results = await searchPosts(query)

  return <section><h1>Results</h1><div>{results.length}</div></section>
}

6. Rebuild and verify

After refactoring, run:

npm run build

The build should complete because the cached code path no longer touches request-scoped routing state.

If you want to review the reproduction project mentioned in the issue, use the example repository.

Common Edge Cases

Passing the whole searchParams object indirectly

Even if you do not read searchParams immediately, passing the object into a cached function is still risky. Extract only the exact primitive values you need.

const sort = typeof searchParams.sort === "string" ? searchParams.sort : "latest"
const data = await getData(sort)

Using headers(), cookies(), or other request APIs in cached code

This problem is not limited to searchParams. Other dynamic server APIs such as headers() and cookies() can create the same category of conflict when used under "use cache".

Non-deterministic helpers

If a cached function reads Date.now(), random values, mutable globals, or environment-specific request state, you may get incorrect caching behavior or build analysis issues. Cacheable functions should be as pure as possible.

Invalid search param types

In real applications, a query parameter may be undefined, an array, or an unexpected string. Always normalize before passing values into cached logic.

function getStringParam(value, fallback = "") {
  return typeof value === "string" ? value : fallback
}

Assuming fetch caching is the same as use cache

Fetch caching and "use cache" are related but not identical. A fetch call can be cached according to its own options, but that does not automatically make it safe to wrap request-bound state in a cached function boundary.

FAQ

1. Why does the build hang instead of showing a clear error?

In some framework versions or code paths, the interaction between route analysis, request-scoped inputs, and cache directives does not fail fast. Instead, the build can become stuck while trying to resolve an invalid rendering/caching mode.

2. Can I still use searchParams and caching together?

Yes. The correct pattern is to read searchParams outside the cached function, validate the values, and pass only plain inputs like strings, numbers, or booleans into the "use cache" function.

3. Should I remove use cache entirely from search pages?

Not necessarily. Keep "use cache" for pure data operations that depend only on normalized inputs. Remove it only from code that directly touches request-time state.

The practical takeaway is straightforward: treat searchParams as an entry-point concern, not a cache-layer concern. Once you isolate URL parsing from cached computation, the build hang disappears and the rendering model becomes predictable.

Leave a Reply

Your email address will not be published. Required fields are marked *