Next.js use cache leak fix: keep Sume job reads out of the cache

Next.js 16.3.8 fixed use cache leaks, including Draft Mode content. A Sume job status read is live, per-workspace data, so fetch it uncached.

5 min readSume
All posts

Should you wrap a Sume job read in use cache in a Next.js app? No. A job's status changes while you watch it, and the response belongs to one workspace, so it should be fetched fresh on the server and never stored in a shared cache. This week's Next.js security release gives a second reason to be strict: it lists two fixes for use cache, one for a root-param cache leak in nested use cache and one for a pending use cache fill that could leak Draft Mode content (Next.js September 2026 security release, read 2026-10-04).

The release shipped on September 30 as 16.3.8 for Active LTS and 15.5.27 for Maintenance LTS. Upgrade first. Then take the cache out of the path that reads job state, because that path is correct without it.

Why a cached job read is wrong even on a patched build

Sume generation is asynchronous by default. A submit returns a job id, and GET /v1/jobs/{id} reports a status until the job is terminal. The status payload carries terminal, result_ready and next_poll_after_seconds, and the docs tell clients to stop polling when terminal is true (Jobs and results). A cached processing response keeps your page showing a spinner after the job has finished.

The reverse failure is worse. A completed job read carries artifact URLs for your workspace. If a cached function is keyed by a job id and called from a request that was not authorized for that id, the cache has turned a private read into a shared one. Authorization belongs on every request, before the read.

What to cache instead

The artifact itself is a stable file once the job is terminal, served from media.sume.com. Cache that at the edge or in your own storage, keyed by the artifact id, and keep the job read dynamic. The handler below forwards a status read with no caching at either layer and passes retry-after hints through.

A Next.js route handler is dynamic when it reads the request, but the explicit no-store below makes the intent obvious to the next reader and to any proxy in front.

export async function GET(
  _req: Request,
  ctx: { params: Promise<{ id: string }> },
) {
  const { id } = await ctx.params;
  if (!/^[A-Za-z0-9_-]{6,80}$/.test(id)) {
    return Response.json({ error: "bad id" }, { status: 400 });
  }

  const upstream = await fetch(`https://api.sume.com/v1/jobs/${id}`, {
    headers: { "x-api-key": process.env.SUME_API_KEY! },
    cache: "no-store",
  });

  const body = await upstream.text();
  return new Response(body, {
    status: upstream.status,
    headers: {
      "content-type": "application/json",
      "cache-control": "private, no-store",
      ...(upstream.headers.get("x-sume-request-id")
        ? { "x-sume-request-id": upstream.headers.get("x-sume-request-id")! }
        : {}),
    },
  });
}

Checklist before you redeploy

  • Search for use cache and unstable_cache wrappers that call api.sume.com and remove them from job and run status paths.
  • Confirm every route that returns a job checks the caller's right to that job id before the upstream call.
  • Forward x-sume-request-id so support can trace a single call.
  • Poll from the server on the interval the status payload asks for, not from every open browser tab.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume