September 17, 2026

Why every visitor gets the same Next.js upload URL

Vijay Sripada
Vijay Sripada
Marketing Lead

Two people upload a video. One of them gets the other's. Or the second upload fails against a URL that has already been consumed. The signed URL was created while the page rendered. Next.js rendered that page exactly once, during next build, then served the resulting HTML to everyone. Nothing about this is a bug in Next.js or in the upload API. It's prerendering doing what it says it does, applied to a value that had to be unique.

TL;DR

A signed upload URL is a one-time grant, not configuration, so it can't be computed at build time. If you create it while a Server Component renders, Next.js prerenders that page and bakes one URL into the HTML for every visitor. Create it in a route handler instead. Pass the uploader a function rather than a URL string, so a fresh URL is minted per upload rather than per build. export const dynamic = "force-dynamic" also works and costs more, because it opts the whole route out of static optimisation to fix one value.

Why a signed upload URL cannot be prerendered

The confusion comes from the URL looking like configuration. It isn't configuration. It's a one-time grant tied to one upload.

Anything treated as build-time constant gets built into the output once. Next.js prerenders Server Components that have no dynamic inputs. That's exactly the optimisation you want for a marketing page. It's exactly wrong for a resource that must differ per request.

ValueSafe to prerender
A product nameYes
A playback ID for a public videoYes
A signed upload URLNo, it is a one-time grant
A signed playback tokenNo, it expires

What you need before you start

  • A Next.js application on the App Router, React 18 or later.
  • A FastPix account with an Access Token ID and Secret Key from activate your account.
  • Credentials in .env.local with no NEXT_PUBLIC_ prefix, which would inline them into the browser bundle.

Create the Next.js upload URL in a route handler

Create the URL in a route handler, which runs per request by definition. Have the client call it when a file is picked:

typescript
// app/api/upload-url/route.ts
import { Fastpix } from "@fastpix/fastpix-node";

const fastpix = new Fastpix({
  security: {
    username: process.env.FASTPIX_USERNAME,
    password: process.env.FASTPIX_PASSWORD,
  },
});

export async function POST() {
  const upload = await fastpix.inputVideo.upload(
    { corsOrigin: "*", pushMediaSettings: { accessPolicy: "public" } },
    { headers: { "X-Client-Type": "web-browser" } },
  );

  if (!("data" in upload)) throw new Error("Could not create an upload.");

  return Response.json({ url: upload.data!.url, mediaId: upload.data!.uploadId });
}

Then give the uploader a function rather than a URL string. That distinction is the whole fix:

text
// app/upload/page.tsx
"use client";

import { FastPixUploader } from "@fastpix/fp-react-uploader";

async function createUpload() {
  const response = await fetch("/api/upload-url", { method: "POST" });
  const { url } = await response.json();
  return url as string;
}

export default function Page() {
  return <FastPixUploader endpoint={createUpload} accept="video/*" />;
}

endpoint accepts a function, and the uploader calls it when a file is picked. A page viewed a thousand times without an upload mints zero URLs. A page that uploads three files mints three.

You can run this against a real upload before committing to it. The free plan covers 10 videos and 100K streaming minutes a month with no credit card.

Why force-dynamic is the worse fix

You can insist on passing a URL string from a Server Component. export const dynamic = "force-dynamic" on that page stops the prerendering and gives each visitor a fresh render.

It works, and it costs more than it looks like. You have opted the entire route out of static optimisation to fix one value. Every visit now runs server code, even for people who will never upload. You're also still minting URLs per page view rather than per upload, which means most of them are created and discarded.

Use force-dynamic when the page genuinely has to be dynamic for other reasons. For this problem the route handler is both cheaper and more correct.

Why the page needs a use client directive

The uploader component ships with its own "use client" banner, so it doesn't need one from you. The directive at the top of that page is there for createUpload.

Functions can't cross the server-to-client boundary. Define a function in a Server Component, pass it as a prop to a client component, and you get Error: Functions cannot be passed directly to Client Components. That applies to endpoint and to every on* callback.

So any file that passes one of those as a prop has to be a client module. That's the actual reason for the directive. Knowing it saves the next round of confusion when someone tries to "clean up" by removing it.

Avoid passing a Server Action as endpoint. It receives the selected File and uploads it to your own server first, which reintroduces the bandwidth path direct upload exists to avoid.

The same prerendering bug on the playback page

Prerendering catches playback too, and it's worth checking while you're here.

The readiness signal that tells you a playback ID exists at all is covered in the video.media.ready webhook. A page that fetches a media's playback ID during render can prerender that fetch. Every visitor then sees whichever video was ready when the build ran. For a public video on a static page this is fine and desirable. For a page that shows the video a user just uploaded, it's the same bug in a different coat.

The general rule: if the value depends on who is asking or on when they ask, it can't be computed at build time. Everything else can.

Get a fresh upload URL per upload

Move the call into app/api/upload-url/route.ts, pass createUpload as endpoint rather than a URL string, and keep the page's "use client" directive where it is. Two visitors now get two URLs, and a visitor who never uploads gets none. The free plan covers 10 videos and 100K streaming minutes a month, no credit card. Open two browsers and confirm the URLs differ.

Frequently Asked Questions (FAQs)

Why is my Next.js upload URL the same for every user?

Because it is being created while the page renders, and Next.js prerendered that page at build time into static HTML. Every visitor receives the same baked-in string. Create the URL in a route handler instead and have the uploader call it through a function. A fresh URL is then minted per upload rather than per build.

How do I stop a Next.js page from being prerendered?

export const dynamic = "force-dynamic" on the page opts that route out of static optimization. It solves this problem and costs more than the alternative because the whole route now runs server code on every visit. A route handler called from the client is cheaper and mints URLs only when someone actually uploads.

Why does my upload endpoint function throw about Server Components?

Because functions cannot cross the server-to-client boundary. Error: Functions cannot be passed directly to Client Components means endpoint or an on* callback is defined in a Server Component. Move that definition into a file marked "use client". The uploader itself already carries its own directive.

Should I use a Server Action for the upload endpoint?

No. A Server Action passed as endpoint receives the selected File and uploads it to your own server first. That routes the video bytes through your infrastructure. Direct upload exists so the browser sends the file straight to storage, and a route handler returning a signed URL keeps that property.

Can I put FastPix credentials in NEXT_PUBLIC_ variables?

No. The NEXT_PUBLIC_ prefix inlines a value into the browser bundle at build time. The Secret Key would be served as a literal string to every visitor. Credentials stay unprefixed in .env.local and are read with process.env inside server code only.

Share

Stay Ahead of Video
Streaming Trends

Start shipping video today.