How to Implement Payload Jobs for Background Operations in Next.js on Vercel
How to Build a Serverless-Friendly Gallery Auto-Sync System in Payload CMS Without Freezing Your App

The Challenge
Imagine you're building a website for a client who wants their gallery to automatically update whenever they upload new images. Sounds simple, right? But here's the catch:
- Performance Problem: Running heavy operations directly in hooks freezes your entire Payload app
- Serverless Constraints: Vercel's serverless environment doesn't support persistent background processes
- User Experience: Users shouldn't wait for complex operations to complete before seeing their upload succeed
The Old Way (That Breaks Everything):
Let's first understand why the traditional approach fails. This code demonstrates the problematic pattern that many developers initially try:
// ❌ DON'T DO THIS - It will freeze your app!
export const syncGalleryBlocks: CollectionAfterChangeHook = async ({ doc, req }) => {
// This runs immediately and blocks everything
const pages = await req.payload.find({ collection: 'pages', limit: 1000 });
// ... heavy processing that takes 5+ seconds
// User's upload appears frozen!
};
Why this breaks: This hook executes synchronously during the upload process, meaning the user's browser waits for the entire gallery sync to complete before showing success. On Vercel's serverless functions, this can cause timeouts and poor user experience.
What You'll Learn
By the end of this guide, you'll have:
✅ A robust job queue system that handles background operations
✅ Serverless-compatible architecture that works perfectly on Vercel
✅ Automatic gallery sync that updates instantly when media is uploaded
✅ Fallback mechanisms ensuring reliability even when things go wrong
✅ Production-ready authentication for secure job execution
Essential Reading
Before diving into the implementation, we highly recommend reading these official Payload documentation pages to understand the underlying concepts:
📚 Jobs Queue Overview - Understanding the fundamentals
📚 Tasks Documentation - How to define and structure tasks
📚 Queues Documentation - Managing job queues effectively
The Solution
We'll build a hybrid system that:
- Queues jobs for background processing
- Triggers immediate execution via HTTP endpoints
- Falls back gracefully when immediate execution fails
- Runs on Vercel without any serverless limitations
Step 1: Understanding the Architecture
The Problem We're Solving
You have two collections:
- Media Collection: Where users upload images
- Pages Collection: Contains gallery blocks that should auto-update
Here's the data structure we're working with:
// Your gallery block structure
{
blockType: 'gallery',
autoSyncMedia: true, // ← This enables auto-sync
images: ['media-id-1', 'media-id-2'], // ← We add new media here
}
What this structure achieves: The autoSyncMedia
boolean flag allows users to control which galleries should automatically receive new uploads. The images
array stores references to media documents that will be displayed in the gallery.
The Flow We're Building
📸 User uploads image
↓
🎯 Hook triggers immediately (non-blocking)
↓
🚀 Try immediate sync via HTTP endpoint
↓
✅ Success? → Done!
↓
❌ Failed? → Queue job + trigger execution
↓
⏰ Vercel cron ensures cleanup every minute
Step 2: Configure Payload Jobs
What we're doing: Setting up the core job system in Payload's configuration. This defines the task that will handle gallery synchronization and establishes the job processing infrastructure.
Key concepts: We're creating a task called syncMediaToGalleries
that takes a mediaId
as input and processes gallery updates. The task includes retry logic, error handling, and comprehensive logging.
// payload.config.ts
import { buildConfig } from 'payload'
export default buildConfig({
// ... your existing config
jobs: {
tasks: [
{
slug: 'syncMediaToGalleries',
label: 'Sync Media to Gallery Blocks',
inputSchema: [
{
name: 'mediaId',
type: 'text',
required: true,
},
],
handler: async ({ input, req }) => {
req.payload.logger.info(`=== GALLERY SYNC JOB STARTED ===`)
req.payload.logger.info(`Starting gallery sync for media ${input.mediaId}`)
try {
// Step 1: Verify media exists and is ready
let mediaDoc
let retries = 3
while (retries > 0) {
try {
mediaDoc = await req.payload.findByID({
collection: 'media',
id: input.mediaId,
})
req.payload.logger.info(`Media found: ${mediaDoc.filename || mediaDoc.id}`)
break
} catch (error) {
retries--
if (retries > 0) {
req.payload.logger.info(`Media ${input.mediaId} not ready, retrying...`)
await new Promise(resolve => setTimeout(resolve, 1000))
} else {
throw new Error(`Media ${input.mediaId} not found after retries`)
}
}
}
if (!mediaDoc) {
req.payload.logger.warn(`Media document ${input.mediaId} not found, skipping`)
return { output: {} }
}
// Step 2: Find pages with gallery blocks that have autoSyncMedia enabled
const pages = await req.payload.find({
collection: 'pages',
depth: 2,
limit: 1000,
})
req.payload.logger.info(`Found ${pages.docs.length} total pages`)
const pagesWithGalleries = pages.docs.filter(page => {
if (!page.layout || !Array.isArray(page.layout)) {
return false
}
const hasGallery = page.layout.some((block: any) => {
const isGallery = block.blockType === 'gallery'
const hasAutoSync = block.autoSyncMedia === true
if (isGallery) {
req.payload.logger.info(`Page ${page.id} has gallery block, autoSync: ${hasAutoSync}`)
}
return isGallery && hasAutoSync
})
return hasGallery
})
req.payload.logger.info(`Found ${pagesWithGalleries.length} pages with auto-sync galleries`)
// Step 3: Update gallery blocks
let pagesUpdated = 0
let galleriesUpdated = 0
for (const page of pagesWithGalleries) {
if (!page.layout || !Array.isArray(page.layout)) continue
let hasUpdates = false
let pageGalleriesUpdated = 0
req.payload.logger.info(`Processing page ${page.id} with ${page.layout.length} blocks`)
const updatedLayout = page.layout.map((block: any) => {
if (block.blockType === 'gallery' && block.autoSyncMedia === true) {
req.payload.logger.info(`Processing gallery block in page ${page.id}`)
// Initialize images array if it doesn't exist
if (!block.images) {
block.images = []
}
// Check if media already exists in gallery
const imageExists = block.images.some((imageId: string) => {
return imageId === input.mediaId
})
if (!imageExists) {
block.images.push(input.mediaId)
hasUpdates = true
pageGalleriesUpdated++
req.payload.logger.info(`Added media ${input.mediaId} to images in page ${page.id}`)
} else {
req.payload.logger.info(`Media ${input.mediaId} already exists in gallery in page ${page.id}`)
}
}
return block
})
if (hasUpdates) {
try {
await req.payload.update({
collection: 'pages',
id: page.id,
data: { layout: updatedLayout },
depth: 0,
overrideAccess: true,
})
pagesUpdated++
galleriesUpdated += pageGalleriesUpdated
req.payload.logger.info(`Updated ${pageGalleriesUpdated} galleries in page ${page.id}`)
} catch (error) {
req.payload.logger.error(`Failed to update page ${page.id}: ${(error as Error).message}`)
}
}
}
const result = {
pagesUpdated,
galleriesUpdated,
mediaId: input.mediaId,
}
req.payload.logger.info(`=== GALLERY SYNC COMPLETED ===`)
req.payload.logger.info(`Result: ${JSON.stringify(result)}`)
return { output: result }
} catch (error) {
req.payload.logger.error(`=== GALLERY SYNC ERROR ===`)
req.payload.logger.error(`Error in gallery sync: ${(error as Error).message}`)
throw error
}
},
},
]
},
// ... rest of your config
})
This system component delivers the following:
Core Functionality:
- Task definition with input validation.
- Retry logic for temporary database issues.
- Logging for debugging and monitoring.
- Error handling that prevents job failures from crashing the system.
- Duplicate prevention to ensure images are not added multiple times.
Key Configuration Details:
- Retry logic: A
while (retries > 0)
loop is used to manage scenarios where media might not be immediately available after upload. - Depth parameter: Setting
depth: 2
ensures retrieval of full block data, rather than just references. - overrideAccess: true: This setting bypasses standard access control for system-level operations.
Further Information: Consult the Tasks Documentation for more details.
Step 3: Create the Media Collection Hook
What we're doing: Creating a hook that triggers when new media is uploaded. This hook implements a hybrid strategy: first attempting immediate execution for speed, then falling back to the job queue for reliability.
Key concepts: The hook uses setTimeout
to avoid blocking the upload response, implements a dual-strategy approach, and includes comprehensive error handling.
// src/collections/Media/hooks/syncGalleryBlocks.ts
import { CollectionAfterChangeHook } from 'payload';
export const syncGalleryBlocks: CollectionAfterChangeHook = async ({
doc,
req,
operation,
}) => {
// Only run for create operations (new uploads)
if (operation !== 'create') {
return doc;
}
try {
// Get the server URL for making HTTP requests
const serverUrl = process.env.NEXT_PUBLIC_SERVER_URL || 'http://localhost:3000';
req.payload.logger.info(`Starting gallery sync process for media ${doc.id}`);
// Strategy 1: Try immediate execution first (faster, more reliable)
setTimeout(async () => {
try {
const immediateResponse = await fetch(`${serverUrl}/api/gallery-sync/immediate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// Add authentication header if CRON_SECRET is available
...(process.env.CRON_SECRET && {
'Authorization': `Bearer ${process.env.CRON_SECRET}`
}),
},
body: JSON.stringify({ mediaId: doc.id }),
});
if (immediateResponse.ok) {
const result = await immediateResponse.json();
req.payload.logger.info(
`Successfully executed immediate gallery sync for media ${doc.id}: ${JSON.stringify(result.result)}`
);
return; // Success - no need for fallback
} else {
const errorText = await immediateResponse.text();
req.payload.logger.warn(
`Immediate gallery sync failed for media ${doc.id}: ${immediateResponse.status} ${errorText}. Falling back to job queue.`
);
}
} catch (immediateError) {
req.payload.logger.warn(
`Immediate gallery sync error for media ${doc.id}: ${(immediateError as Error).message}. Falling back to job queue.`
);
}
// Strategy 2: Fallback to job queue system
try {
// Queue the job using the Local API (this still works for queuing)
const job = await req.payload.jobs.queue({
task: 'syncMediaToGalleries',
input: {
mediaId: doc.id,
},
waitUntil: new Date(Date.now() + 2000), // Wait 2 seconds
});
req.payload.logger.info(
`Queued gallery sync task ${job.id} for media ${doc.id} as fallback`
);
// Trigger job execution via HTTP endpoint
setTimeout(async () => {
try {
const jobResponse = await fetch(`${serverUrl}/api/payload-jobs/run`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(process.env.CRON_SECRET && {
'Authorization': `Bearer ${process.env.CRON_SECRET}`
}),
},
});
if (jobResponse.ok) {
const result = await jobResponse.json();
req.payload.logger.info(
`Successfully triggered job execution for media ${doc.id}: ${JSON.stringify(result)}`
);
} else {
const errorText = await jobResponse.text();
req.payload.logger.warn(
`Failed to trigger job execution for media ${doc.id}: ${jobResponse.status} ${errorText}`
);
}
} catch (jobError) {
req.payload.logger.warn(
`Error triggering job execution for media ${doc.id}: ${(jobError as Error).message}`
);
}
}, 2000); // Wait 2 seconds for job execution
} catch (queueError) {
req.payload.logger.error(
`Failed to queue gallery sync task for media ${doc.id}: ${(queueError as Error).message}`
);
}
}, 1000); // Wait 1 second to ensure database transaction is complete
} catch (error) {
req.payload.logger.error(
`Failed to initiate gallery sync for media ${doc.id}: ${(error as Error).message}`
);
}
return doc;
};
What this achieves:
- Non-blocking uploads - Users see immediate success
- Dual-strategy reliability - Immediate execution with job queue fallback
- Serverless compatibility - Uses HTTP endpoints instead of Local API
- Graceful degradation - System continues working even if one strategy fails
Complex parts explained:
- setTimeout usage: Prevents blocking the upload response while allowing background processing
- Conditional authentication: Only adds auth headers when
CRON_SECRET
is available - Nested setTimeout: Creates a delay between job queuing and execution triggering
- operation !== 'create': Ensures the hook only runs for new uploads, not updates
Step 4: Add the Hook to Your Media Collection
What we're doing: Integrating the gallery sync hook into your Media collection configuration. This is where we tell Payload to run our custom logic whenever media documents change.
// src/collections/Media.ts
import { CollectionConfig } from 'payload';
import { syncGalleryBlocks } from './Media/hooks/syncGalleryBlocks';
export const Media: CollectionConfig = {
slug: 'media',
labels: {
singular: 'Media',
plural: 'Media',
},
hooks: {
afterChange: [syncGalleryBlocks], // ← Add your hook here
},
upload: {
// ... your upload config
},
fields: [
// ... your fields
],
};
What this achieves:
- Automatic triggering - Hook runs every time media is created or updated
- Clean integration - Follows Payload's hook pattern
- Maintainable code - Hook logic is separated into its own file
Why afterChange: This hook fires after the media document is successfully saved to the database, ensuring the media ID is available for processing.
Step 5: Create the Immediate Execution Endpoint
What we're doing: Building a dedicated API endpoint that can execute gallery sync operations immediately without queuing delays. This endpoint duplicates the job logic but runs it directly for faster response times.
Key concepts: This endpoint provides authentication, input validation, and executes the same logic as our job handler but synchronously.
// src/app/api/gallery-sync/immediate/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { getPayload } from 'payload'
import config from '@payload-config'
// Authentication helper
function isAuthenticated(request: NextRequest): boolean {
const cronSecret = process.env.CRON_SECRET;
if (cronSecret) {
const authHeader = request.headers.get('authorization');
const expectedAuth = `Bearer ${cronSecret}`;
if (authHeader !== expectedAuth) {
return false;
}
}
return true;
}
export async function POST(request: NextRequest) {
const startTime = Date.now();
try {
// Check authentication
if (!isAuthenticated(request)) {
return NextResponse.json(
{ success: false, error: 'Unauthorized' },
{ status: 401 }
);
}
const { mediaId } = await request.json();
if (!mediaId) {
return NextResponse.json(
{ success: false, error: 'mediaId is required' },
{ status: 400 }
);
}
const payload = await getPayload({ config });
payload.logger.info(`=== IMMEDIATE GALLERY SYNC STARTED ===`);
payload.logger.info(`Media ID: ${mediaId}`);
// Execute the same logic as the job handler
// (Copy the job handler logic here for immediate execution)
const executionTime = Date.now() - startTime;
const result = {
pagesUpdated: 1, // Your actual results
galleriesUpdated: 1,
mediaId,
executionTimeMs: executionTime,
};
payload.logger.info(`=== IMMEDIATE GALLERY SYNC COMPLETED ===`);
payload.logger.info(`Result: ${JSON.stringify(result)}`);
return NextResponse.json({
success: true,
result,
timestamp: new Date().toISOString(),
});
} catch (error) {
const executionTime = Date.now() - startTime;
console.error('Error in immediate gallery sync:', error);
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
executionTimeMs: executionTime,
timestamp: new Date().toISOString(),
},
{ status: 500 }
);
}
}
export async function GET(request: NextRequest) {
// Allow GET requests with mediaId as query parameter
const { searchParams } = new URL(request.url);
const mediaId = searchParams.get('mediaId');
if (!mediaId) {
return NextResponse.json(
{ success: false, error: 'mediaId query parameter is required' },
{ status: 400 }
);
}
// Create a mock request with the mediaId in the body
const mockRequest = new Request(request.url, {
method: 'POST',
headers: request.headers,
body: JSON.stringify({ mediaId }),
});
return POST(mockRequest as NextRequest);
}
What this achieves:
- Immediate execution - No queuing delays for critical operations
- Security - Authentication prevents unauthorized access
- Flexibility - Supports both GET and POST requests
- Monitoring - Includes execution time metrics
- Error handling - Comprehensive error responses
Complex parts explained:
- Authentication function: Checks for
CRON_SECRET
but allows access if not set (for development) - GET support: Allows testing via browser by converting GET requests to POST
- Performance tracking:
startTime
andexecutionTime
help monitor performance - Mock request creation: Enables GET requests to reuse POST logic
Note: In the actual implementation, you would copy the complete job handler logic where the comment indicates.
Step 6: Enhance the Job Execution Endpoint
What we're doing: Improving the standard Payload jobs endpoint with better authentication, logging, and monitoring capabilities. This endpoint processes queued jobs and provides detailed execution information.
Key concepts: This endpoint serves as the bridge between Vercel's cron jobs and Payload's job processing system.
// src/app/api/payload-jobs/run/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { getPayload } from 'payload'
import config from '@payload-config'
// Authentication helper
function isAuthenticated(request: NextRequest): boolean {
const cronSecret = process.env.CRON_SECRET;
if (cronSecret) {
const authHeader = request.headers.get('authorization');
const expectedAuth = `Bearer ${cronSecret}`;
if (authHeader !== expectedAuth) {
return false;
}
}
return true;
}
export async function POST(request: NextRequest) {
const startTime = Date.now();
try {
// Check authentication
if (!isAuthenticated(request)) {
return NextResponse.json(
{ success: false, error: 'Unauthorized' },
{ status: 401 }
);
}
const payload = await getPayload({ config })
payload.logger.info('=== JOB EXECUTION ENDPOINT CALLED ===');
// Run jobs with a limit
const result = await payload.jobs.run({
limit: 10,
queue: 'default',
})
const executionTime = Date.now() - startTime;
payload.logger.info(`=== JOB EXECUTION COMPLETED ===`);
payload.logger.info(`Execution time: ${executionTime}ms`);
payload.logger.info(`Result: ${JSON.stringify(result)}`);
return NextResponse.json({
success: true,
result,
executionTimeMs: executionTime,
timestamp: new Date().toISOString(),
})
} catch (error) {
const executionTime = Date.now() - startTime;
console.error('Error running jobs:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
executionTimeMs: executionTime,
timestamp: new Date().toISOString(),
},
{ status: 500 }
)
}
}
export async function GET(request: NextRequest) {
// Allow GET requests for Vercel cron
return POST(request)
}
What this achieves:
- Secure job processing - Authentication prevents unauthorized job execution
- Performance monitoring - Tracks execution time for optimization
- Comprehensive logging - Detailed logs for debugging
- Cron compatibility - Supports both GET and POST for Vercel cron jobs
- Error resilience - Proper error handling and reporting
Complex parts explained:
- limit: 10: Processes up to 10 jobs per execution to prevent timeout issues
- queue: 'default': Specifies which queue to process (you can have multiple queues)
- GET support: Vercel cron jobs can use GET requests, so we redirect to POST logic
📚 Learn more: Queues Documentation
Step 7: Configure Vercel Cron Jobs
What we're doing: Setting up Vercel's cron job system to automatically process queued jobs every minute. This ensures that even if immediate execution fails, jobs will eventually be processed.
Key concepts: Vercel cron jobs run on a schedule and call specific API endpoints in your application.
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"crons": [
{
"path": "/api/payload-jobs/run",
"schedule": "*/1 * * * *"
}
]
}
What this achieves:
- ✅ Automatic job processing - Jobs run every minute without manual intervention
- ✅ Reliability - Ensures no jobs are left unprocessed
- ✅ Serverless compatibility - Works perfectly with Vercel's infrastructure
Schedule explanation: */1 * * * *
means "every 1 minute". You can adjust this based on your needs:
*/5 * * * *
= every 5 minutes0 * * * *
= every hour0 0 * * *
= daily at midnight
Step 8: Set Up Your Gallery Block
What we're doing: Configuring your gallery block to support the auto-sync feature. This adds the necessary fields that control whether a gallery should automatically receive new uploads.
Key concepts: The autoSyncMedia
checkbox allows users to enable/disable auto-sync per gallery, while the images
field stores the actual media references.
// src/blocks/general/Gallery/config.ts
import type { Block } from 'payload';
const GalleryBlock: Block = {
slug: 'gallery',
interfaceName: 'GalleryBlock',
labels: {
singular: 'Gallery',
plural: 'Galleries',
},
fields: [
{
name: 'autoSyncMedia',
type: 'checkbox',
label: 'Auto-sync new media uploads',
defaultValue: false,
admin: {
description: 'When enabled, new uploaded images will automatically be added to this gallery',
},
},
{
name: 'images',
type: 'upload',
relationTo: 'media',
hasMany: true,
label: 'Gallery Images',
},
// ... other fields
]
};
export default GalleryBlock;
What this achieves:
- User control - Editors can choose which galleries auto-sync
- Clear interface - Descriptive labels explain the functionality
- Flexible design - Can be added to existing gallery blocks
- Safe defaults - Auto-sync is disabled by default
Field explanations:
- autoSyncMedia: Boolean field that controls auto-sync behavior
- hasMany: true: Allows multiple images in a single gallery
- relationTo: 'media': Creates proper relationships to media documents
Step 9: Environment Variables
What we're doing: Configuring the environment variables needed for authentication and proper URL resolution in both development and production environments.
Set these in your Vercel project dashboard under Settings > Environment Variables:
# Required for production security
CRON_SECRET=your-secure-random-string-here
# Your Vercel deployment URL
NEXT_PUBLIC_SERVER_URL=https://your-domain.vercel.app
What this achieves:
- Security -
CRON_SECRET
prevents unauthorized job execution - Environment flexibility - Different URLs for development and production
- Proper routing - Ensures HTTP requests go to the correct endpoints
Security best practices:
- Generate
CRON_SECRET
using a cryptographically secure method - Use at least 32 characters for the secret
- Never commit secrets to version control
- Rotate secrets periodically
Step 10: Testing Your Implementation
What we're doing: Creating a comprehensive test script that verifies all components of your gallery sync system are working correctly.
Key concepts: This script tests both immediate execution and job queue fallback, providing detailed feedback about system performance.
// scripts/test-gallery-sync.js
#!/usr/bin/env node
const https = require('https');
const http = require('http');
const SERVER_URL = process.env.NEXT_PUBLIC_SERVER_URL || 'http://localhost:3000';
const CRON_SECRET = process.env.CRON_SECRET;
const MEDIA_ID = process.argv[2];
if (!MEDIA_ID) {
console.error('Usage: node scripts/test-gallery-sync.js <mediaId>');
process.exit(1);
}
async function testGallerySync() {
console.log('🧪 Testing Gallery Sync System');
console.log(`Testing with Media ID: ${MEDIA_ID}`);
try {
// Test immediate sync
const response = await fetch(`${SERVER_URL}/api/gallery-sync/immediate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(CRON_SECRET && { 'Authorization': `Bearer ${CRON_SECRET}` }),
},
body: JSON.stringify({ mediaId: MEDIA_ID }),
});
const result = await response.json();
if (result.success) {
console.log('✅ Gallery sync successful!');
console.log(`Pages updated: ${result.result.pagesUpdated}`);
console.log(`Galleries updated: ${result.result.galleriesUpdated}`);
} else {
console.log('❌ Gallery sync failed:', result.error);
}
} catch (error) {
console.error('Error testing gallery sync:', error.message);
}
}
testGallerySync();
Run it with:
node scripts/test-gallery-sync.js YOUR_MEDIA_ID
What this achieves:
- System verification - Confirms all components work together
- Performance insights - Shows execution times and results
- Debugging aid - Helps identify issues quickly
- Documentation - Serves as usage example
Script features:
- Command-line interface - Easy to use with any media ID
- Environment detection - Works in both development and production
- Error handling - Provides clear feedback on failures
- Authentication support - Automatically includes auth headers when available
What You've Achieved
🎉 Congratulations! You now have:
✅ Non-blocking Media Uploads
Users can upload images instantly without waiting for gallery processing. The upload completes in under 1 second while gallery sync happens in the background.
✅ Automatic Gallery Updates
New images appear in galleries automatically without manual intervention. The system intelligently finds galleries with auto-sync enabled and adds new media.
✅ Serverless Compatibility
Everything works perfectly on Vercel's serverless infrastructure. No persistent processes or long-running servers required.
✅ Robust Error Handling
Multiple fallback mechanisms ensure reliability. If immediate execution fails, the job queue takes over. If that fails, cron jobs provide cleanup.
✅ Production Security
Authentication system protects your endpoints from unauthorized access while remaining flexible for development.
Real-World Benefits
Before (The Broken Way):
- ❌ 5-10 second upload delays
- ❌ App freezes during processing
- ❌ Poor user experience
- ❌ Doesn't work on Vercel
After (Your New System):
- ✅ Instant uploads (< 1 second)
- ✅ Background processing
- ✅ Excellent user experience
- ✅ Perfect Vercel compatibility
Pro Tips
🔧 Development
# Test locally without authentication
unset CRON_SECRET
npm run dev
🚀 Production
# Always set CRON_SECRET for security
vercel env add CRON_SECRET
📊 Monitoring
Check your Vercel function logs to monitor job execution:
=== IMMEDIATE GALLERY SYNC STARTED ===
Media ID: 111
Media found: my-image.png
Found 1 pages with auto-sync galleries
Added media 111 to images in page 1
=== IMMEDIATE GALLERY SYNC COMPLETED ===
Troubleshooting
Images Not Syncing?
- Check gallery blocks have
autoSyncMedia: true
- Verify media ID exists in admin panel
- Check Vercel function logs for errors
Authentication Errors?
- Verify
CRON_SECRET
is set correctly - Check authorization header format:
Bearer ${secret}
Performance Issues?
- Monitor execution time metrics
- Consider reducing page query limits
- Check database connection timeouts
Next Steps
Now that you have a working system, consider these enhancements:
- Batch Processing: Handle multiple uploads efficiently
- Selective Sync: Allow per-gallery configuration
- Admin Interface: Visual job queue management
- Webhooks: Notify external systems of updates
Further Learning
To deepen your understanding of Payload's job system, explore these resources:
📚 Jobs Queue Overview - Comprehensive guide to job concepts
📚 Tasks Documentation - Advanced task configuration and patterns
📚 Queues Documentation - Queue management and optimization strategies
Thanks, Matija