---
title: "Run Payload CMS Jobs on Vercel: Complete 5‑Step Setup"
slug: "run-payload-cms-jobs-on-vercel"
published: "2025-12-26"
updated: "2026-01-03"
categories:
  - "Payload"
tags:
  - "Payload CMS jobs on Vercel"
  - "Vercel cron jobs"
  - "vercel.json crons"
  - "CRON_SECRET"
  - "/api/payload-jobs/run"
  - "jobs.autoRun"
  - "serverless background jobs"
  - "Payload job handlers"
  - "generate-blur handler"
  - "Next.js vercel crons"
  - "TypeScript Payload jobs"
llm-intent: "how-to"
audience-level: "intermediate"
llm-purpose: "Payload CMS jobs on Vercel: configure Vercel Cron, vercel.json, and CRON_SECRET to trigger /api/payload-jobs/run so queued jobs process reliably — follow…"
llm-prereqs:
  - "Payload CMS"
  - "Vercel"
  - "vercel.json"
  - "Next.js"
  - ".next"
  - "TypeScript"
  - "pnpm"
  - "curl"
---

**Summary Triples**
- (Run Payload CMS Jobs on Vercel: Complete 5‑Step Setup, expresses-intent, how-to)
- (Run Payload CMS Jobs on Vercel: Complete 5‑Step Setup, covers-topic, Payload CMS jobs on Vercel)
- (Run Payload CMS Jobs on Vercel: Complete 5‑Step Setup, provides-guidance-for, Payload CMS jobs on Vercel: configure Vercel Cron, vercel.json, and CRON_SECRET to trigger /api/payload-jobs/run so queued jobs process reliably — follow…)

### {GOAL}
Payload CMS jobs on Vercel: configure Vercel Cron, vercel.json, and CRON_SECRET to trigger /api/payload-jobs/run so queued jobs process reliably — follow…

### {PREREQS}
- Payload CMS
- Vercel
- vercel.json
- Next.js
- .next
- TypeScript
- pnpm
- curl

### {STEPS}
1. Enable jobs in Payload config
2. Implement job handlers
3. Create vercel.json with crons
4. Set CRON_SECRET environment variable
5. Secure jobs.access.run
6. Deploy, verify, and troubleshoot

<!-- llm:goal="Payload CMS jobs on Vercel: configure Vercel Cron, vercel.json, and CRON_SECRET to trigger /api/payload-jobs/run so queued jobs process reliably — follow…" -->
<!-- llm:prereq="Payload CMS" -->
<!-- llm:prereq="Vercel" -->
<!-- llm:prereq="vercel.json" -->
<!-- llm:prereq="Next.js" -->
<!-- llm:prereq=".next" -->
<!-- llm:prereq="TypeScript" -->
<!-- llm:prereq="pnpm" -->
<!-- llm:prereq="curl" -->

# Run Payload CMS Jobs on Vercel: Complete 5‑Step Setup
> Payload CMS jobs on Vercel: configure Vercel Cron, vercel.json, and CRON_SECRET to trigger /api/payload-jobs/run so queued jobs process reliably — follow…
Matija Žiberna · 2025-12-26

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.

```typescript
// 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:

```typescript
// 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.

```json
{
  "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:

```typescript
// 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:
```bash
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:
```typescript
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