Run a task later in Next.js on Vercel
Schedule a one-off HTTP callback after a user action. Handle delivery, retries, and cancellation in a Next.js route.
Schedule a request after a user action
You want to expire an invitation in fifteen minutes, publish a post tomorrow, or send a reminder before an appointment. Save the task in your database, then ask Cuey to call your Next.js route at its due time.
Choose how to run the task
Use Vercel Cron for a shared recurring job, such as checking for due tasks. Vercel defines those schedules in deployment configuration and uses UTC. With a polling job, you own the due-task query, retries, and duplicate handling.
Use a scheduler API for individual requests that users create while your app runs. Cuey accepts a future timestamp and callback URL. Upstash QStash also offers delayed HTTP delivery. Choose based on the delivery behaviour and limits your app needs.
1. Schedule from your server
Install cuey with npm install cuey. Set CUEY_API_KEY, APP_ORIGIN to your production HTTPS origin, and TASK_WEBHOOK_SECRET in Vercel's environment settings. Generate a secret with openssl rand -hex 32. Keep these values on the server.
// lib/schedule-task.ts
import "server-only";
import { cuey } from "cuey";
// Call after checking that the signed-in user owns this task.
export async function scheduleTask(taskId: string, runAt: Date) {
if (!Number.isFinite(runAt.getTime()) || runAt.getTime() <= Date.now()) {
throw new Error("Choose a future date.");
}
const origin = process.env.APP_ORIGIN;
const secret = process.env.TASK_WEBHOOK_SECRET;
if (!origin || !secret) throw new Error("Missing webhook configuration.");
return cuey.schedule({
webhook_url: new URL("/api/run-task", origin).href,
method: "POST",
scheduled_at: runAt.toISOString(),
headers: { Authorization: `Bearer ${secret}` },
payload: { task_id: taskId },
});
}
// Schedule fifteen minutes from now, then save event.id with the task.
// const event = await scheduleTask(task.id, new Date(Date.now() + 900000));Call this function from your authenticated route or server action after checking task ownership. Save the returned event ID. Your callback URL must accept requests from Cuey; a protected preview deployment or localhost needs a reachable test endpoint.
2. Handle the callback
This route checks the callback secret and task ID. Implement runTask in your app before using it. Load the current task state there, so an old callback cannot publish a cancelled post.
// app/api/run-task/route.ts
// Implement runTask against your application's database.
import { runTask } from "@/lib/tasks";
export async function POST(request: Request) {
const secret = process.env.TASK_WEBHOOK_SECRET;
if (!secret || request.headers.get("authorization") !== `Bearer ${secret}`) {
return new Response("Unauthorized", { status: 401 });
}
let body: unknown;
try { body = await request.json(); }
catch { return new Response("Invalid JSON", { status: 400 }); }
if (!body || typeof body !== "object" || !("task_id" in body)
|| typeof body.task_id !== "string" || !body.task_id) {
return new Response("Missing task_id", { status: 400 });
}
// Load the task, check cancellation, and deduplicate before side effects.
await runTask(body.task_id);
return Response.json({ ok: true });
}Return a 2xx response after the work succeeds. Configure Cuey's retry settings for failures. Use durable deduplication in your database before a side effect; a network timeout can occur after your handler finishes the work.
3. Cancel or change the time
To cancel, mark the task as cancelled in your database and delete its pending Cuey event. To change the time, save the new task state and replace the pending event. Keep a schedule version with the task if an old callback could race with the replacement.
The webhook guide covers event inspection and cancellation. For repeated calls, use a recurring schedule with the user's timezone.
Schedule your first request
The first 10,000 events each month are free. Additional events cost $0.0001 each. See pricing or read the API documentation.
Get your API key ↗