Cron Jobs & Scheduled Tasks
Temps includes a built-in cron scheduler that lets you run recurring tasks by invoking HTTP endpoints on a schedule. Define your cron jobs in your repository's .temps.yaml file—no external services needed.
Competitive Advantage: While platforms like Vercel require separate services (Vercel Cron requires Pro plan), Temps includes unlimited scheduled tasks on every project and environment.
Quick Start
Step 1: Create .temps.yaml File
Create a .temps.yaml file in your repository root:
cron:
- path: "/api/cleanup"
schedule: "0 * * * *"
name: "Hourly Cleanup"
Step 2: Deploy Your Application
When you deploy, Temps automatically:
- Reads your
.temps.yamlfile - Configures the cron jobs
- Starts scheduling them
Step 3: Create Your Endpoint
Create the endpoint that will be invoked:
// app/api/cleanup/route.ts (Next.js)
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
// Verify cron secret (recommended)
const authHeader = request.headers.get('authorization');
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// Perform cleanup task
await cleanupOldRecords();
await archiveExpiredSessions();
return NextResponse.json({
success: true,
message: 'Cleanup completed',
timestamp: new Date().toISOString()
});
}
That's it! Your cron job will run automatically on the schedule you defined.
Configuration File
Cron jobs are defined in a .temps.yaml file in your project directory.
File Location
The .temps.yaml file should be placed in your project's root directory. For most projects, this is the repository root:
your-repo/
├── .temps.yaml ← Place here (repository root)
├── src/
├── package.json
└── ...
For monorepos, if your project is in a subdirectory, place .temps.yaml in that subdirectory:
monorepo/
├── apps/
│ └── my-app/
│ ├── .temps.yaml ← Place here (project directory)
│ ├── src/
│ └── package.json
└── packages/
The file location matches your project's configured directory in Temps.
Basic Format
cron:
- path: "/api/cleanup"
schedule: "0 * * * *"
name: "Hourly Cleanup" # Optional: descriptive name
Multiple Cron Jobs
cron:
- path: "/api/cleanup"
schedule: "0 * * * *"
name: "Hourly Cleanup"
- path: "/api/backup"
schedule: "0 2 * * *"
name: "Daily Backup"
- path: "/api/digest"
schedule: "0 9 * * *"
name: "Daily Email Digest"
Full Configuration Example
# .temps.yaml
cron:
- path: "/api/cleanup"
schedule: "0 * * * *"
name: "Hourly Cleanup"
- path: "/api/backup"
schedule: "0 2 * * *"
name: "Daily Backup at 2 AM"
- path: "/api/weekly-report"
schedule: "0 9 * * 1"
name: "Weekly Report on Mondays"
# Other configuration options
build:
dockerfile: Dockerfile
env:
NODE_ENV: production
How It Works
- Add
.temps.yamlto your project directory (repository root or subdirectory for monorepos) - Commit and push to Git
- Deploy your application
- After deployment completes, Temps reads the
.temps.yamlfile - Cron jobs are configured automatically for that environment
- Jobs run on the specified schedule
Timing: Cron jobs are configured after your deployment is live. The configuration job runs automatically as part of the deployment workflow.
Cron Expression Syntax
Temps uses standard cron expression format with 5 fields:
┌───────────── minute (0 - 59)
│ ┌───────────── hour (0 - 23)
│ │ ┌───────────── day of month (1 - 31)
│ │ │ ┌───────────── month (1 - 12)
│ │ │ │ ┌───────────── day of week (0 - 6) (Sunday to Saturday)
│ │ │ │ │
│ │ │ │ │
* * * * *
Common Schedules
- Name
Every 5 minutes- Description
schedule: "*/5 * * * *"
- Name
Every hour at minute 0- Description
schedule: "0 * * * *"
- Name
Daily at 2:30 AM- Description
schedule: "30 2 * * *"
- Name
Weekly on Monday at 9 AM- Description
schedule: "0 9 * * 1"
- Name
Monthly on the 1st at midnight- Description
schedule: "0 0 1 * *"
- Name
Every weekday at 8 AM- Description
schedule: "0 8 * * 1-5"
- Name
Twice daily (6 AM and 6 PM)- Description
schedule: "0 6,18 * * *"
Use crontab.guru to validate your cron expressions and see human-readable descriptions.
Endpoint Invocation
HTTP Method
Temps invokes your endpoints using GET requests by default.
Authentication
Temps automatically includes an Authorization header when invoking cron endpoints:
Authorization: Bearer <TEMPS_CRON_SECRET>
Set your cron secret as an environment variable:
# In Temps dashboard: Project Settings > Environment Variables
CRON_SECRET=your-secret-key-here
Verify in your endpoint:
Verify Cron Secret
// app/api/cleanup/route.ts
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
// Verify cron secret
const authHeader = request.headers.get('authorization');
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// Perform cleanup task
await cleanupOldRecords();
return NextResponse.json({ success: true });
}
Security: Always validate the Authorization header in your cron endpoints to prevent unauthorized invocations.
Per-Environment Scheduling
Cron jobs defined in .temps.yaml are configured per environment when you deploy. This means:
- Same
.temps.yamlfile - The file is shared across all environments - Environment-specific execution - Each environment (dev, staging, production) runs its own cron jobs
- Isolated execution - Production and staging crons don't interfere with each other
- Configured after deployment - Cron jobs are set up automatically after each deployment completes
Example: Different Behavior Per Environment
While the schedule is the same, you can make your endpoint behave differently based on environment:
// app/api/backup/route.ts
export async function GET(request: Request) {
const env = process.env.NODE_ENV;
if (env === 'production') {
// Production: Full backup every 6 hours
await performFullBackup();
} else {
// Staging: Lightweight backup daily
await performLightBackup();
}
return Response.json({ success: true });
}
# .temps.yaml
cron:
- path: "/api/backup"
schedule: "0 */6 * * *" # Every 6 hours (production will do full backup)
name: "Database Backup"
Common Use Cases
1. Database Backups
Schedule regular database backups:
# .temps.yaml
cron:
- path: "/api/backup"
schedule: "0 2 * * *" # Daily at 2 AM
name: "Database Backup"
// app/api/backup/route.ts
export async function GET(request: Request) {
// Verify auth...
const { S3Client, PutObjectCommand } = await import('@aws-sdk/client-s3');
// Dump database
const dump = await dumpDatabase();
// Upload to S3
const s3 = new S3Client({ /* config */ });
await s3.send(new PutObjectCommand({
Bucket: 'backups',
Key: `database-${Date.now()}.sql`,
Body: dump
}));
return Response.json({ success: true });
}
2. Data Cleanup
Remove old records, expired sessions, or temporary files:
# .temps.yaml
cron:
- path: "/api/cleanup"
schedule: "0 3 * * *" # Daily at 3 AM
name: "Daily Cleanup"
// app/api/cleanup/route.ts
export async function GET(request: Request) {
// Verify auth...
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
// Delete expired sessions
await db.session.deleteMany({
where: { expiresAt: { lt: sevenDaysAgo } }
});
return Response.json({ success: true });
}
3. Email Digests
Send daily/weekly email summaries:
# .temps.yaml
cron:
- path: "/api/digest"
schedule: "0 9 * * *" # Daily at 9 AM
name: "Daily Email Digest"
4. Cache Warming
Pre-populate caches before peak traffic:
# .temps.yaml
cron:
- path: "/api/warm-cache"
schedule: "0 6 * * *" # Daily at 6 AM
name: "Cache Warming"
5. API Sync
Synchronize data from external APIs:
# .temps.yaml
cron:
- path: "/api/sync"
schedule: "*/15 * * * *" # Every 15 minutes
name: "External API Sync"
Monitoring & Logs
View Cron Jobs
In the Temps dashboard:
- Go to Project Settings → Cron Jobs
- See all configured cron jobs
- View execution history
- Check success/failure rates
Execution Details
Each execution includes:
- Start Time - When the job began
- Duration - How long it took
- Status - Success, Failed, or Timeout
- HTTP Status - Response code from endpoint
- Response Body - Endpoint response (first 1KB)
- Error Message - If the job failed
View Logs
Check your application logs to see cron job execution:
# View deployment logs
temps logs --project my-app --environment production
# Filter for cron-related logs
temps logs --project my-app | grep "cron"
Error Handling
Retries
Failed cron jobs are automatically retried:
- 1st retry: After 1 minute
- 2nd retry: After 5 minutes
- 3rd retry: After 15 minutes
After 3 failed attempts, the job is marked as failed.
Handling Failures in Code
Return appropriate status codes:
// app/api/cron/task/route.ts
export async function GET(request: Request) {
try {
await performTask();
return Response.json({ success: true }, { status: 200 });
} catch (error) {
if (error instanceof TransientError) {
// Will be retried (5xx errors)
return Response.json({ error: error.message }, { status: 500 });
} else {
// Won't be retried (4xx errors)
return Response.json({ error: error.message }, { status: 400 });
}
}
}
Retry Logic: Only 5xx server errors trigger retries. 4xx client errors are not retried.
Timeout
Cron jobs have a default timeout of 5 minutes. If your job takes longer, it will be terminated.
For long-running tasks, consider:
- Breaking them into smaller jobs
- Using background job queues
- Processing in batches
Best Practices
- Name
Idempotency- Description
Design cron jobs to be idempotent—running them multiple times should produce the same result:
// ✅ Good: Idempotent await db.session.deleteMany({ where: { expiresAt: { lt: sevenDaysAgo } } }); // ❌ Bad: Not idempotent await db.user.updateMany({ data: { credits: { increment: 10 } } });
- Name
Use Locking- Description
Prevent concurrent executions with distributed locks:
import { redis } from '@/lib/redis'; const lockKey = 'cron:backup:lock'; const acquired = await redis.set(lockKey, '1', 'EX', 3600, 'NX'); if (!acquired) { return Response.json({ error: 'Already running' }, { status: 409 }); } try { await performBackup(); } finally { await redis.del(lockKey); }
- Name
Monitor Duration- Description
Track how long jobs take and log if they exceed expected duration:
const start = Date.now(); await performTask(); const duration = Date.now() - start; if (duration > 5 * 60 * 1000) { // 5 minutes console.warn(`Job took ${duration}ms`); }
- Name
Test Locally- Description
Test cron endpoints locally before deploying:
# Invoke endpoint manually curl http://localhost:3000/api/cleanup \ -H "Authorization: Bearer your-cron-secret"
Troubleshooting
- Name
Cron not executing- Description
Possible causes:
- Invalid cron expression (use crontab.guru)
.temps.yamlfile not in correct location (should match project directory)- Endpoint path doesn't match
- Environment not deployed
- YAML syntax error in
.temps.yaml
Solution:
- Verify
.temps.yamlis in your project directory (check project settings for directory path) - Check YAML syntax is valid
- Check cron syntax with crontab.guru
- Ensure your endpoint path matches exactly (including leading slash)
- Redeploy your application (cron configuration happens after deployment)
- Check deployment logs for cron configuration messages
- Name
401 Unauthorized errors- Description
Cause: Missing or incorrect
CRON_SECRETenvironment variable.Solution:
- Set
CRON_SECRETin your environment variables - Verify it matches what your endpoint expects
- Check the Authorization header format
- Set
- Name
Timeout errors- Description
Cause: Job takes longer than 5-minute default timeout.
Solution:
- Break long-running tasks into smaller jobs
- Use background job queues for heavy processing
- Process data in batches
- Name
Jobs running multiple times- Description
Cause: Multiple environments deployed with same schedule.
Solution: This is expected behavior—each environment runs its own cron jobs. Use locking (see Best Practices) if you need to prevent concurrent execution.
Next Steps
- Environment Variables - Store cron secrets securely
- Monitoring - Track cron job health
- Error Tracking - Catch cron endpoint errors
- Logs - Debug failed executions