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…

·Updated on:·Matija Žiberna·
Run Payload CMS Jobs on Vercel: Complete 5‑Step Setup

📚 Comprehensive Payload CMS Guides

Detailed Payload guides with field configuration examples, custom components, and workflow optimization tips to speed up your CMS development process.

No spam. Unsubscribe anytime.

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/run when jobs are configured.
  • schedule: A standard cron expression. In this example, */5 * * * * means "run every 5 minutes." Common schedules:
    • 0 * * * * = every hour
    • 0 0 * * * = once daily
    • */15 * * * * = every 15 minutes
    • 0 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:

  1. Go to your Vercel project dashboard
  2. Navigate to Settings → Environment Variables
  3. Click Add New
  4. Create a variable called CRON_SECRET with a random value (16+ characters is recommended)
  5. 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:

  1. Commit your vercel.json changes
  2. Push to your Vercel-connected Git repository
  3. 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:

  1. Verify vercel.json is in your project root and committed to Git. Vercel reads this from your repository, not from local files.
  2. 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.
  3. Ensure your deployment completed successfully. A failed deploy won't have the updated config.

Authorization failures or 401 errors:

  1. Go to Vercel → Settings → Environment Variables
  2. Verify CRON_SECRET is set and has a value (not empty)
  3. Verify the secret is set for all environments (Production, Preview, Development)
  4. Copy the exact secret value and test manually with curl to ensure there are no whitespace issues

Endpoint returns 404:

  1. Confirm you have the jobs configuration in your payload.config.ts with actual tasks defined
  2. Without tasks, Payload won't create the /api/payload-jobs/run endpoint
  3. Rebuild and redeploy your application

Jobs are queued but still not executing:

  1. Navigate to your Payload admin → Jobs collection
  2. Check if you see "queued" jobs. If jobs appear queued but never complete, the cron might be hitting the endpoint but handlers are failing.
  3. Check your application logs for errors during job execution
  4. Review your handler code in src/payload/jobs/ to ensure it's not throwing errors

Handler errors during execution:

  1. 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

3

Frequently Asked Questions

Comments

Leave a Comment

Your email will not be published

10-2000 characters

• Comments are automatically approved and will appear immediately

• Your name and email will be saved for future comments

• Be respectful and constructive in your feedback

• No spam, self-promotion, or off-topic content

Matija Žiberna
Matija Žiberna
Full-stack developer, co-founder

I'm Matija Žiberna, a self-taught full-stack developer and co-founder passionate about building products, writing clean code, and figuring out how to turn ideas into businesses. I write about web development with Next.js, lessons from entrepreneurship, and the journey of learning by doing. My goal is to provide value through code—whether it's through tools, content, or real-world software.