Run Payload CMS Jobs on Vercel: Complete 5‑Step Setup
Step-by-step guide to configure Vercel Cron, vercel.json, CRON_SECRET, and Payload's /api/payload-jobs/run endpoint…

📚 Comprehensive Payload CMS Guides
Detailed Payload guides with field configuration examples, custom components, and workflow optimization tips to speed up your CMS development process.
You've queued up jobs in your Payload CMS config. Everything looks right. You hit queue. Then... nothing. No errors. No failures. Just crickets.
If you're running Payload CMS on Vercel's serverless platform, this is exactly what happens when you try to use the jobs.autoRun property. The problem isn't your code—it's that autoRun was designed for dedicated servers that are always running. On Vercel, your application spins up for requests and spins down when idle. There's no "always running" process to check the job queue.
The solution is to use Vercel's Cron job feature to periodically hit your /api/payload-jobs/run endpoint, which tells Payload to process any queued jobs. This guide walks you through exactly how to set it up from scratch.
Why autoRun Doesn't Work on Serverless
The jobs.autoRun property in Payload assumes a persistent process is running. On Vercel, each function invocation is isolated and ephemeral. Between requests, nothing is running. Your jobs sit in the queue, waiting for someone to explicitly tell Payload to run them.
Vercel Crons solve this by making HTTP requests to your application on a schedule. When the cron hits your jobs endpoint, Payload wakes up, processes the queue, and goes back to sleep. It's not continuous monitoring, but it's reliable and cost-effective.
Step 0: Configure Jobs in Your Payload Config
Before Vercel can trigger job processing, your Payload configuration must actually have jobs enabled with handlers defined.
// File: payload.config.ts
import { buildConfig } from 'payload'
import type { PayloadRequest } from 'payload'
// Import your job handlers
import { generateBlurHandler } from '@/payload/jobs/generate-blur'
const config = buildConfig({
// ... other config ...
jobs: {
access: {
run: ({ req }: { req: PayloadRequest }): boolean => {
// Allow logged in users to execute this endpoint
if (req.user) return true
// Check for Vercel Cron secret (we'll set this up in Step 3)
const authHeader = req.headers.get('authorization')
if (!process.env.CRON_SECRET) {
console.warn('CRON_SECRET environment variable is not set')
return false
}
return authHeader === `Bearer ${process.env.CRON_SECRET}`
},
queue: () => true,
cancel: () => true,
},
jobsCollectionOverrides: ({ defaultJobsCollection }) => ({
...defaultJobsCollection,
access: {
...defaultJobsCollection.access,
read: ({ req }) => !!req.user,
create: ({ req }: { req: PayloadRequest }) => !!req.user,
update: ({ req }: { req: PayloadRequest }) => !!req.user,
delete: ({ req }: { req: PayloadRequest }) => !!req.user,
},
}),
tasks: [
{
slug: 'generate-blur',
inputSchema: [
{ name: 'docId', type: 'text', required: true },
{ name: 'collection', type: 'text', required: true },
],
outputSchema: [
{ name: 'message', type: 'text', required: true },
],
handler: generateBlurHandler,
retries: 1,
},
// Add more tasks as needed
],
},
})
export default config
This configuration:
access.run: Determines who can trigger job execution. We allow logged-in users and requests with the correct CRON_SECRET header (which Vercel will send).
tasks: An array of job definitions. Each task needs a unique slug, input/output schemas, a handler function that does the actual work, and optionally retries. When jobs are queued, they reference one of these slugs.
handler: The actual function that executes when a job runs. Create handlers in your jobs directory (e.g., src/payload/jobs/generate-blur.ts). Here's a minimal example:
// File: src/payload/jobs/generate-blur.ts
import type { Payload } from 'payload'
export const generateBlurHandler = async ({
payload,
job,
}: {
payload: Payload
job: any
}) => {
// Extract job inputs
const { docId, collection } = job.input
// Do your actual work here
console.log(`Processing ${collection} document ${docId}`)
// Your logic: optimize images, generate metadata, etc.
const result = await yourProcessingFunction(docId, collection)
return {
message: `Successfully processed ${docId}`,
}
}
Without this foundational setup, the Vercel cron will hit an endpoint that has nothing to execute. Make sure your handlers are actually defined and imported.
Step 1: Create Your vercel.json Configuration
Vercel's cron feature is configured through a vercel.json file in your project root. This file tells Vercel which endpoint to hit and how often.
{
"buildCommand": "pnpm run build",
"outputDirectory": ".next",
"env": {
"VERCEL_SKIP_BUILD": "1"
},
"crons": [
{
"path": "/api/payload-jobs/run",
"schedule": "*/5 * * * *"
}
]
}
Breaking down each property:
buildCommand: The command Vercel runs to build your application. This is standard for Next.js projects.
outputDirectory: Where your built application lives (.next for Next.js).
env: Environment variables applied during the cron execution. VERCEL_SKIP_BUILD: "1" tells Vercel to skip rebuilding for cron invocations—you only want the already-built code to run. This keeps your cron invocations fast.
crons: An array of scheduled tasks. Each entry specifies:
- path: The endpoint Vercel will call. Payload automatically provides
/api/payload-jobs/runwhen jobs are configured. - schedule: A standard cron expression. In this example,
*/5 * * * *means "run every 5 minutes." Common schedules:0 * * * *= every hour0 0 * * *= once daily*/15 * * * *= every 15 minutes0 9 * * 1= every Monday at 9 AM
The schedule is in UTC, so plan accordingly if your Vercel project is in a different timezone.
Step 2: Set Up the CRON_SECRET Environment Variable
Running a publicly accessible endpoint that processes your jobs is a security risk. Vercel provides a way to authenticate cron requests using environment variables.
Add a new environment variable to your Vercel project:
- Go to your Vercel project dashboard
- Navigate to Settings → Environment Variables
- Click Add New
- Create a variable called
CRON_SECRETwith a random value (16+ characters is recommended) - Ensure it's available in all environments (Production, Preview, Development)
Example value: your_random_secret_string_here_12345
Vercel automatically makes this variable available to cron requests as an Authorization header in the format Bearer <CRON_SECRET>.
Step 3: Secure Your Jobs Endpoint
The access control is already in your payload.config.ts from Step 0. When Vercel's cron triggers the endpoint, it automatically includes the CRON_SECRET as an Authorization header. Your access function validates it.
Here's how it works:
// From payload.config.ts jobs.access.run
if (req.user) return true // Allow logged-in users
// Verify Vercel Cron secret
const authHeader = req.headers.get('authorization')
if (!process.env.CRON_SECRET) {
console.warn('CRON_SECRET environment variable is not set')
return false
}
return authHeader === `Bearer ${process.env.CRON_SECRET}`
Logged-in users can trigger jobs manually from the Payload admin UI. The req.user check allows this.
Vercel's cron requests include the secret in an Authorization header in the format Bearer <CRON_SECRET>. Your function compares this to the environment variable. If they match, the request succeeds. If the secret is missing or wrong, the request is denied.
This prevents unauthorized people from triggering your background jobs. Only Vercel (which knows your secret) and logged-in users can invoke the job runner.
Step 4: Deploy and Verify
Once you've set up both files and the environment variable:
- Commit your
vercel.jsonchanges - Push to your Vercel-connected Git repository
- Vercel redeploys your project with the new configuration
To verify it's working:
Check your Vercel project logs: Go to your Vercel dashboard, find your project, and look at the Functions or Runtime Logs section. You should see requests to /api/payload-jobs/run appearing at your scheduled interval.
Check your Payload admin: Navigate to the Jobs collection in your Payload admin UI. If jobs are being processed, you'll see them move from "queued" to "completed" status at your cron interval.
Manual test: You can manually trigger the jobs endpoint by making a request with your secret:
curl -H "Authorization: Bearer your_cron_secret_here" \
https://your-project.vercel.app/api/payload-jobs/run
If you see a successful response, your configuration is working.
Troubleshooting
No jobs are running at all:
- Verify
vercel.jsonis in your project root and committed to Git. Vercel reads this from your repository, not from local files. - Check Vercel logs: Go to your Vercel dashboard → project → Logs. Look for successful requests to
/api/payload-jobs/run. If you don't see any requests, the cron isn't triggering. - Ensure your deployment completed successfully. A failed deploy won't have the updated config.
Authorization failures or 401 errors:
- Go to Vercel → Settings → Environment Variables
- Verify
CRON_SECRETis set and has a value (not empty) - Verify the secret is set for all environments (Production, Preview, Development)
- Copy the exact secret value and test manually with curl to ensure there are no whitespace issues
Endpoint returns 404:
- Confirm you have the
jobsconfiguration in yourpayload.config.tswith actual tasks defined - Without tasks, Payload won't create the
/api/payload-jobs/runendpoint - Rebuild and redeploy your application
Jobs are queued but still not executing:
- Navigate to your Payload admin → Jobs collection
- Check if you see "queued" jobs. If jobs appear queued but never complete, the cron might be hitting the endpoint but handlers are failing.
- Check your application logs for errors during job execution
- Review your handler code in
src/payload/jobs/to ensure it's not throwing errors
Handler errors during execution:
- Your handler function might be throwing an exception. Add try-catch and logging:
export const myHandler = async ({ payload, job }) => {
try {
// Your logic here
} catch (error) {
console.error('Job failed:', error)
throw error // Payload will retry based on your config
}
}
What Happens Next
Now that your jobs are actually running on schedule, here's what you have:
Jobs queued in your application (via Payload's job API) will automatically sit in the queue.
Vercel's cron periodically hits /api/payload-jobs/run on your configured schedule.
Payload processes the queue, running your handlers and moving jobs from "queued" to "completed" or "failed" status.
Your handlers do the actual work—optimizing images, generating AI content, updating vectors, or whatever background tasks you've defined. Each handler runs with full access to your Payload instance and database.
The cricket silence is gone. Your background jobs are now running reliably on Vercel.
Thanks, Matija