Next.js 16.4 GET route handler: poll a Sume job at request time

With Cache Components, Next.js 16.4 runs GET handlers at request time. A Sume job status route stays live if it sends no-store and follows the poll hint.

4 min readSume
All posts

A GET route that proxies a Sume job status stays live in Next.js 16.4 as long as it reads something per request, such as a query parameter or a header. The Route Handlers page (version 16.4.0) says that with Cache Components, GET handlers run at request time by default and prerendering stops when the handler touches request.body or headers(). Your browser can then poll the route and see fresh status.

This post shows a status route that forwards the poll, passes through the wait hint Sume returns, and avoids the one mistake that serves a stale processing state.

What the 16.4 docs say about GET

Before Cache Components, a GET handler with no dynamic input could be prerendered under force-static. The docs still list that opt-in, and they say other methods are never cached. In 16.4 the recommended mode runs GET at request time unless you opt in.

Do not add use cache inside the handler body; the docs say it cannot sit directly there. If you cache a helper, never cache the Sume status call.

Route handler caching rules for a status poll, read 2026-10-08 (Next.js 16.4.0 docs)
MethodCached by defaultHow to opt in
GETNoexport const dynamic = 'force-static'
POSTNeverNot possible
GET with Cache ComponentsRuns at request timeuse cache in a helper, not the handler body
GET that reads headers()Prerender stopsNothing to do

What the Sume status payload gives you

A Sume job envelope returns status (queued, processing, completed, failed, canceled), terminal, result_ready and next_poll_after_seconds. Pass the wait hint to the browser instead of picking your own interval.

Poll only until terminal is true. A timeout on your side does not cancel the job, and you should not submit again; a retry of the submit uses the same Idempotency-Key.

A status route

The route reads the job id from the URL and forwards one request. The Sume client sends the key in x-api-key; do not add an Authorization header as well.

export async function GET(request: Request) {
  const id = new URL(request.url).searchParams.get("job");
  const key = process.env.SUME_API_KEY;
  if (!id || !key) return Response.json({ error: "bad request" }, { status: 400 });

  const res = await fetch(`https://api.sume.com/v1/jobs/${encodeURIComponent(id)}`, {
    headers: { "x-api-key": key },
    cache: "no-store",
  });
  const body = await res.json();
  return Response.json(body, {
    status: res.status,
    headers: { "cache-control": "no-store" },
  });
}

Limits to plan for

Status reads can hit 429 rate_limited; treat that as poll backpressure and read retry-after. For long jobs, prefer a webhook and use this route only as a fallback for the page that shows progress.

  • Read next_poll_after_seconds and return it to the client.
  • Send cache-control: no-store so a CDN never stores a processing response.
  • Never expose the API key to browser code.

Choose the polling client

A browser that polls your route should wait for the value your route returns, not a fixed 2 seconds. Return next_poll_after_seconds in your own response and have the client schedule its next call from it. If the job is queued, Sume may ask for a longer interval than when it is processing, and a long queue is exactly when a fixed fast loop hurts.

Stop on terminal. Then, if result_ready is true, make a second request for the result so that your status route stays small. Media URLs on media.sume.com are durable, so the result page can be cached after the job ends without a staleness problem for the file itself.

  • Do not poll from many tabs; one poll per job per user is enough.
  • Do not retry a failed status read faster than retry-after allows.
  • Show queued as waiting, not as an error; a full concurrency limit still accepts the job.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume