Convex Cron Jobs
Basic Cron Setup
// convex/crons.ts
import { cronJobs } from "convex/server";
import { internal } from "./_generated/api";
const crons = cronJobs();
// Run every hour
crons.interval(
"cleanup expired sessions",
{ hours: 1 },
internal.tasks.cleanupExpiredSessions,
{}
);
// Run every day at midnight UTC
crons.cron(
"daily report",
"0 0 * * *",
internal.reports.generateDailyReport,
{}
);
export default crons;
Interval-Based Scheduling
// Every 5 minutes
crons.interval("sync data", { minutes: 5 }, internal.sync.fetchData, {});
// Every 2 hours
crons.interval("cleanup temp", { hours: 2 }, internal.files.cleanup, {});
// Every 30 seconds (minimum)
crons.interval("health check", { seconds: 30 }, internal.monitoring.check, {});
Cron Expression Scheduling
┌───────────── minute (0-59)
│ ┌───────────── hour (0-23)
│ │ ┌───────────── day of month (1-31)
│ │ │ ┌───────────── month (1-12)
│ │ │ │ ┌───────────── day of week (0-6, Sunday=0)
* * * * *
// Every day at 9 AM UTC
crons.cron("morning digest", "0 9 * * *", internal.notifications.morning, {});
// Every Monday at 8 AM UTC
crons.cron("weekly summary", "0 8 * * 1", internal.reports.weekly, {});
// First day of month at midnight
crons.cron("monthly billing", "0 0 1 * *", internal.billing.process, {});
// Every 15 minutes
crons.cron("frequent sync", "*/15 * * * *", internal.sync.run, {});
Internal Functions for Crons
Always use internal functions:
// convex/tasks.ts
import { internalMutation } from "./_generated/server";
import { v } from "convex/values";
export const cleanupExpiredSessions = internalMutation({
args: {},
returns: v.number(),
handler: async (ctx) => {
const oneHourAgo = Date.now() - 60 * 60 * 1000;
const expired = await ctx.db
.query("sessions")
.withIndex("by_lastActive")
.filter((q) => q.lt(q.field("lastActive"), oneHourAgo))
.collect();
for (const session of expired) {
await ctx.db.delete(session._id);
}
return expired.length;
},
});
Crons with Arguments
crons.interval(
"cleanup temp files",
{ hours: 1 },
internal.cleanup.byType,
{ fileType: "temp", maxAge: 3600000 }
);
crons.interval(
"cleanup cache files",
{ hours: 24 },
internal.cleanup.byType,
{ fileType: "cache", maxAge: 86400000 }
);
Batching Large Datasets
Handle large datasets to avoid timeouts:
const BATCH_SIZE = 100;
export const processBatch = internalMutation({
args: { cursor: v.optional(v.string()) },
returns: v.null(),
handler: async (ctx, args) => {
const result = await ctx.db
.query("items")
.withIndex("by_status", (q) => q.eq("status", "pending"))
.paginate({ numItems: BATCH_SIZE, cursor: args.cursor ?? null });
for (const item of result.page) {
await ctx.db.patch(item._id, { status: "processed", processedAt: Date.now() });
}
// Schedule next batch if more items
if (!result.isDone) {
await ctx.scheduler.runAfter(0, internal.tasks.processBatch, {
cursor: result.continueCursor,
});
}
return null;
},
});
External API Calls
Use actions for external APIs:
// convex/sync.ts
"use node";
import { internalAction, internalMutation } from "./_generated/server";
import { internal } from "./_generated/api";
import { v } from "convex/values";
export const syncExternalData = internalAction({
args: {},
returns: v.null(),
handler: async (ctx) => {
const response = await fetch("https://api.example.com/data", {
headers: { Authorization: `Bearer ${process.env.API_KEY}` },
});
const data = await response.json();
await ctx.runMutation(internal.sync.storeData, { data, syncedAt: Date.now() });
return null;
},
});
// In crons.ts
crons.interval("sync external", { minutes: 15 }, internal.sync.syncExternalData, {});
Logging and Monitoring
export const cleanupWithLogging = internalMutation({
args: {},
returns: v.null(),
handler: async (ctx) => {
const startTime = Date.now();
let processedCount = 0;
try {
const items = await ctx.db.query("items")
.filter((q) => q.lt(q.field("expiresAt"), Date.now()))
.collect();
for (const item of items) {
await ctx.db.delete(item._id);
processedCount++;
}
await ctx.db.insert("cronLogs", {
jobName: "cleanup",
duration: Date.now() - startTime,
processedCount,
status: "success",
});
} catch (error) {
await ctx.db.insert("cronLogs", {
jobName: "cleanup",
duration: Date.now() - startTime,
processedCount,
status: "failed",
error: String(error),
});
throw error;
}
return null;
},
});
Common Pitfalls
- Using public functions - Always use internal functions
- Forgetting timezone - All cron expressions use UTC
- Long-running mutations - Break into batches
- Missing error handling - Log failures for debugging
References
- Cron Jobs: https://docs.convex.dev/scheduling/cron-jobs
- Scheduling: https://docs.convex.dev/scheduling