Queuebase
← Back to blog

How to Run Cron Jobs in Next.js on Vercel (and When You Actually Need a Job Queue)

By Brock Herion

If you’re searching for “Next.js cron jobs,” you’re probably not trying to learn cron syntax. You just want something in your app to run later without becoming a pain in the ass to maintain.

That might be a daily digest, a cleanup task, a nightly sync, or a weekly report. I’ve seen people treat all of those as the same problem. They are not. Some of them are just scheduled endpoints. Some of them are background jobs.

This guide walks through the four practical options:

  • Vercel Cron for simple scheduled endpoints
  • node-cron for persistent Node servers you control
  • QStash for scheduled HTTP task delivery
  • Queuebase when you need retries, visibility, typed payloads, and both scheduled and on-demand jobs

If you want the short version:

  • use Vercel Cron if all you need is “hit this route every day”
  • use node-cron only if you run a persistent Node process
  • use Queuebase if the work matters enough that retries, history, and visibility are part of the requirement

What “cron jobs in Next.js” usually means

Next.js is an application framework. It is not a scheduler.

When developers search for cron jobs in Next.js, they usually want one of these outcomes:

  • run something every hour, day, or week
  • move slow work out of a request or server action
  • avoid serverless timeouts
  • retry failures automatically
  • see whether the work actually ran

Cron solves one part of that: run this on a schedule.

The moment you care about retries, logs, job state, manual triggers, or payloads, you are not really choosing a scheduler anymore. You are choosing how background work is going to function in your app.

The first question: where is your app running?

Before you pick a tool, answer one question: is your app running on a persistent Node process, or on a serverless platform like Vercel?

That determines almost everything:

  • On serverless, you cannot assume a process stays alive long enough for an in-memory scheduler to keep ticking.
  • On a persistent server, you can run an always-on scheduler inside the process, but you still need to think about restarts, duplication across multiple instances, and job visibility.

If you are on Vercel, start with tools that are built for serverless execution. If you are self-hosting a long-lived Node app, you have more options.

Option 1: Use Vercel Cron for simple scheduled endpoints

Vercel Cron Jobs are the cleanest answer when you’re already on Vercel and your requirement is genuinely just “hit this endpoint on a schedule.”

This is a good fit when:

  • the task can be kicked off by hitting a route on a schedule
  • there is no per-run payload beyond whatever the route can derive itself
  • the work is short enough to stay within your execution limits
  • you do not need a full queue with job lifecycle tracking

Here’s the shape of it:

{
  "crons": [
    {
      "path": "/api/reports/daily",
      "schedule": "0 9 * * *"
    }
  ]
}
// app/api/reports/daily/route.ts
export async function GET() {
  await generateDailyReport();

  return Response.json({ ok: true });
}

That works well for simple maintenance tasks, report generation, cache warming, and other work that can be handled cleanly inside one request.

Where it starts to break down:

  • you need retries with backoff
  • you need to track job status over time
  • you want to trigger the same work on-demand and on a schedule
  • you want typed job payloads instead of route-specific logic
  • the task grows beyond “run this endpoint every so often”

Vercel Cron is a scheduler, not a job queue. That sounds nitpicky, but that’s the difference between choosing the right tool and backing yourself into a corner.

If you want a deeper breakdown, see Queuebase vs Vercel Cron Jobs.

Option 2: Use node-cron only if you control a persistent Node process

node-cron is fine when you run a traditional always-on Node server. It is lightweight, familiar, and easy to wire up. It is also the thing people recommend way too casually.

import cron from "node-cron";

cron.schedule("0 * * * *", async () => {
  await cleanupExpiredSessions();
});

This approach makes sense when:

  • you deploy to a VPS, Docker container, or long-lived server process
  • you want a minimal dependency
  • your scheduling needs are simple

This approach does not make sense if you expect it to behave reliably on Vercel or another serverless platform. A serverless function is not a persistent worker. It starts, runs, and exits. There is no guarantee your in-memory scheduler is still around when the next minute or hour rolls over.

Even on persistent infrastructure, node-cron comes with tradeoffs:

  • process restarts can interrupt scheduling
  • multiple app instances can accidentally run the same job twice
  • retries, logs, and history are all your responsibility
  • the scheduled function itself is still just application code, not a tracked job

If your app is small and self-hosted, node-cron may be enough. If you are on Vercel, I would not use it for this.

Option 3: Use QStash for scheduled HTTP tasks

QStash sits in the middle. It is useful when you want a managed service to schedule and deliver HTTP requests reliably without building that layer yourself.

That makes it a better fit than raw cron when:

  • the unit of work is naturally an HTTP request
  • you want managed delivery semantics
  • you do not want to run your own worker infrastructure

It is a good tool for scheduled webhooks, delayed callbacks, and HTTP-first task delivery.

But QStash is not trying to be a full Next.js background job framework. It does not give you a typed job router, job lifecycle abstractions, or a strong model for defining and monitoring jobs inside your app.

That means QStash is often strongest when:

  • you already think in HTTP endpoints
  • you are comfortable building more of the application-level abstraction yourself

If you want a direct side-by-side comparison, see Queuebase vs QStash.

Option 4: Use Queuebase when you need more than a timer

At some point, the problem stops being “how do I run this every day?” and becomes “how do I run this reliably, and how do I know it actually worked?”

That is where Queuebase fits.

I built Queuebase because this was the gap I kept running into with Next.js apps. I did not just want to hit an endpoint on a timer. I wanted background jobs with type safety, retries, logs, and a clean developer experience.

Queuebase is a background job system for Next.js and Node.js apps. You define jobs with typed inputs, enqueue them from your app, and optionally schedule eligible jobs to run automatically.

Use Queuebase when you need any combination of:

  • scheduled jobs
  • on-demand jobs triggered by user actions
  • retries and backoff
  • logs and visibility
  • typed payloads
  • one system for both scheduling and queueing

In practice, that usually means one of these scenarios:

  • a scheduled job kicks off work that you also want to run manually
  • an important task fails and you need retries instead of silent failure
  • someone on your team asks “did this run?” and you need a better answer than “I think so”
  • you do not want to rebuild scheduling, queueing, and monitoring as three separate concerns

Here is a minimal router that mixes scheduled and on-demand work:

// src/jobs/index.ts
import { createJobRouter, job } from "@queuebase/nextjs";
import { z } from "zod";

export const jobs = createJobRouter({
  sendWelcomeEmail: job({
    input: z.object({
      to: z.string().email(),
    }),
    handler: async ({ input }) => {
      await sendWelcomeEmail(input.to);
      return { sent: true };
    },
    defaults: {
      retries: 3,
      backoff: "exponential",
    },
  }),

  cleanupExpiredSessions: job({
    input: z.object({}),
    handler: async () => {
      await cleanupExpiredSessions();
      return { cleaned: true };
    },
    schedule: "every hour",
  }),
});

That one router gives you both:

  • scheduled work via schedule
  • on-demand work via .enqueue()

This is the key conceptual difference from plain cron. Instead of bolting a timer onto your app, you define background work as first-class jobs.

That is a better long-term fit for most product code, because the scheduled part is rarely the hard part. Reliability is the hard part.

Minimal Queuebase setup in Next.js

If you want one system for both scheduled jobs and regular background jobs, Queuebase’s setup is small.

App Router

Define the callback route that Queuebase uses to execute jobs:

// src/app/api/queuebase/route.ts
import { createHandler } from "@queuebase/nextjs/handler";
import { jobs } from "@/jobs";

export const POST = createHandler(jobs);

Create a type-safe client:

// src/jobs/client.ts
import { createClient } from "@queuebase/nextjs";
import { jobs } from "./index";

export const jobClient = createClient(jobs, {
  apiUrl: process.env.QUEUEBASE_API_URL ?? "http://localhost:3847",
  apiKey: process.env.QUEUEBASE_API_KEY,
  callbackUrl:
    process.env.QUEUEBASE_CALLBACK_URL ??
    "http://localhost:3000/api/queuebase",
});

Enqueue from anywhere server-side:

await jobClient.sendWelcomeEmail.enqueue({
  to: "user@example.com",
});

Pages Router

If you are on the Pages Router, use createPagesHandler instead:

// pages/api/queuebase.ts
import { createPagesHandler } from "@queuebase/nextjs/pages";
import { jobs } from "@/jobs";

export const config = { api: { bodyParser: false } };
export default createPagesHandler(jobs);

That gives you the same core model on both routing systems.

For the full setup, see the getting started guide.

How to choose

Here is the practical version:

OptionBest forWorks on VercelPayloadsRetriesVisibilityBreaks down when
Vercel CronSimple scheduled endpointsYesRoute-specific/manualManualMinimalYou need queue semantics, status tracking, or reuse between scheduled and on-demand work
node-cronSelf-hosted persistent serversNot reliablyManualManualNoneYou move to serverless or run multiple app instances
QStashScheduled or delayed HTTP tasksYesRequest-basedDelivery-focusedLimited app-level visibilityYou want typed jobs and first-class job abstractions
QueuebaseScheduled and on-demand background jobsYesTypedBuilt-inBuilt-inYou need complex durable workflows rather than straightforward job processing

If you are scanning, this is the section to pay attention to.

Another way to think about it:

  • If you just need “call this endpoint every day,” use Vercel Cron.
  • If you self-host a single Node process and want the lightest possible option, node-cron can be enough.
  • If you want managed HTTP scheduling and delivery, look at QStash.
  • If you want background jobs as a real subsystem in your Next.js app, use Queuebase.

The most common mistake here is choosing based on what sounds smallest today instead of what will still make sense three months from now.

Common mistakes

These are the failure modes that show up over and over:

1. Treating cron like a job queue

Running a route on a schedule is not the same as having a persistent record of a job, retries, logs, and state transitions.

2. Assuming node-cron will work the same on Vercel

It will not. In-memory schedulers depend on a process staying alive.

3. Putting long-running work directly in a request

Even if a scheduled route triggers correctly, the work it kicks off can still be a bad fit for a single request lifecycle.

4. Ignoring idempotency

Scheduled work should be safe to retry. If a task can run twice and corrupt data, your architecture is fragile.

5. Having no visibility into whether the work actually ran

This is the hidden cost of the simplest setup. The less structure you have around jobs, the harder it is to answer “did this run?” when something goes wrong.

FAQ

Can Next.js run cron jobs on Vercel?

Yes. The usual approach is Vercel Cron, which triggers a production endpoint on a schedule.

Does node-cron work on Vercel?

Not reliably. node-cron expects a persistent Node process, which is not how Vercel’s serverless execution model works.

What is the difference between Vercel Cron and a job queue?

Vercel Cron is a scheduling mechanism. A job queue adds concepts like persistence, retries, job history, payloads, and visibility into execution state.

When should I use a queue instead of cron?

Use a queue when the work matters enough that you need retries, status tracking, logs, or the ability to run the same task both on-demand and on a schedule.

Can I run scheduled jobs and on-demand jobs in the same system?

Yes. That is one of the strongest reasons to use Queuebase instead of treating scheduling and background work as separate concerns.

The short version

Most Next.js apps start with a timer and eventually need a queue.

If you only need a scheduled endpoint and your task is simple, Vercel Cron is a solid default. If you run a persistent Node server, node-cron can be perfectly reasonable. If you want managed HTTP scheduling, QStash is worth considering.

But if your app needs scheduled jobs, on-demand background work, retries, typed payloads, and visibility in one place, start with Queuebase. That is the point where “cron jobs in Next.js” stops being a scheduler question and becomes an architecture question.

If you want to compare the broader landscape, read The Best Background Job Libraries for Next.js in 2026. If you already know you want a job queue built for Next.js, start with Queuebase.