Next.js
since: 1.7
@runwisp/js runs scheduled tasks inside a self-hosted Next.js app: next start, output: "standalone" or Docker. It needs a process that keeps running,
so it does not work on Vercel or other serverless hosts. withRunWisp() fails
the build on Vercel. The .env.local that vercel env pull writes doesn’t
count, so next dev still works with it.
It works with Next.js 15 or later, on Node.js or on Bun.
Set up
Section titled “Set up”npm install @runwisp/jsWrap your Next config with withRunWisp(). It keeps the package out of Next’s
server bundle, so it finds the runwisp binary, and copies the binary into
output: "standalone" builds:
import type { NextConfig } from "next";import { withRunWisp } from "@runwisp/js/next";
const nextConfig: NextConfig = {};
export default withRunWisp(nextConfig);Define tasks
Section titled “Define tasks”Put the tasks in their own module:
import { RunWisp } from "@runwisp/js";import { cleanupSessions } from "./sessions";
const runwisp = new RunWisp({ exitOnSignal: Boolean(process.env.NEXT_MANUAL_SIG_HANDLE) });
runwisp.task("cleanup-sessions", { cron: "0 * * * *" }, async () => { const removed = await cleanupSessions(); console.log(`removed ${removed} expired sessions`);});Load it from instrumentation.ts, which Next runs once when the server starts:
export async function register() { if (process.env.NEXT_RUNTIME === "nodejs") await import("./lib/tasks");}next dev and next start print the dashboard address on startup. See the
JavaScript SDK for every task option.
Import lib/tasks.ts only from instrumentation.ts, not from pages or route
handlers. Next loads separate copies of your modules for instrumentation and
for requests, so a database client or cache the tasks use is not the one your
routes use. Keep shared clients on globalThis if that matters.
next dev does not reload instrumentation.ts or what it imports. Restart the
dev server after you change a task.
Vercel Cron
Section titled “Vercel Cron”scheduleVercelCrons() runs the crons from vercel.json under RunWisp. Keep
the file and your cron routes as they are:
import { scheduleVercelCrons } from "@runwisp/js/next";import vercel from "../vercel.json";
scheduleVercelCrons(runwisp, vercel);Each cron becomes a task named after its path, so /api/cron/cleanup is
api-cron-cleanup. On schedule, RunWisp requests the path on
http://localhost:$PORT like Vercel does, with Authorization: Bearer $CRON_SECRET when CRON_SECRET is set. The response goes to the run’s log, and
a status other than 2xx fails the run. Schedules are in UTC, as on Vercel. Pass
{ baseUrl } as the third argument if the app listens elsewhere.
As on Vercel, times missed while the app was down are not run late, and a run
that comes while the last one is still going starts anyway. The tasks get
catch_up = 0 and
max_concurrent = 1024.
Shut down cleanly
Section titled “Shut down cleanly”On SIGTERM and Ctrl-C, next start exits right away. A run in progress then
fails with the app disconnected, and RunWisp stops 10 seconds later. To let
runs finish first, set NEXT_MANUAL_SIG_HANDLE, which turns on
exitOnSignal in the example above. Like
Next, the example treats any value as set, false included:
{ "scripts": { "start": "NEXT_MANUAL_SIG_HANDLE=true next start" } }Standalone output
Section titled “Standalone output”Build on the platform you deploy to, for example inside your Docker image. The
binary is installed for the machine that runs npm install.
In Docker, set HOSTNAME=0.0.0.0. Docker sets HOSTNAME to the container ID,
so a standalone server listens on that address only, and
scheduleVercelCrons() cannot reach it on localhost.
Run Next with Bun’s runtime:
bun --bun next devbun --bun next startA standalone build starts with bun .next/standalone/server.js.