"Job still running from last minute" — without distributed locking, multiple servers run the same cron job simultaneously and data gets corrupted. Claude Code generates the complete safe cron infrastructure.
CLAUDE.md for Cron Job Rules
## Cron Job Design Rules
### Duplicate Prevention (required)
- Prevent concurrent runs of the same job (distributed lock)
- Implement lock with Redis NX+EX (TTL = 2x max job duration)
- Skip if previous job still running (don't error — just skip)
### Error Handling
- Send Slack/PagerDuty alert on failure
- Escalate alert after N consecutive failures
- Record job execution history in DB
### Scheduler
- Use node-cron or BullMQ repeatable jobs
- Make cron schedule configurable via env variables
- Always specify timezone explicitly (avoid defaulting to UTC)
Generating Cron Infrastructure
Design scheduled jobs system.
Jobs:
- Daily at 3 AM: Delete expired sessions
- Every hour: Check subscription expiry
- Every minute: Send pending notifications
Requirements:
- Distributed lock (no double-run across servers)
- DB execution history
- Slack notification on failure
Generate: src/jobs/
Generated:
// src/jobs/jobRunner.ts
import cron from 'node-cron';
import { createClient } from 'redis';
const redis = createClient({ url: process.env.REDIS_URL });
export async function runWithLock(
jobName: string,
timeoutMs: number,
handler: () => Promise<void>
): Promise<void> {
const lockKey = `cron:lock:${jobName}`;
const lockTtl = Math.ceil((timeoutMs * 2) / 1000);
// Acquire distributed lock
const acquired = await redis.set(lockKey, '1', {
NX: true, // Only set if not exists
EX: lockTtl,
});
if (!acquired) {
logger.info({ jobName }, 'Job already running, skipping');
return;
}
const startedAt = new Date();
try {
await handler();
await recordJobRun(jobName, 'success', startedAt);
} catch (err) {
await recordJobRun(jobName, 'failed', startedAt, String(err));
await notifySlack(`❌ Cron job failed: ${jobName}\n${err}`);
throw err;
} finally {
await redis.del(lockKey);
}
}
// src/jobs/definitions.ts
const TIMEZONE = process.env.CRON_TIMEZONE ?? 'Asia/Tokyo';
export function startCronJobs(): void {
// Daily at 3 AM: cleanup expired sessions
cron.schedule(
process.env.CLEANUP_SCHEDULE ?? '0 3 * * *',
() => runWithLock('cleanup-sessions', 30 * 60 * 1000, cleanupExpiredSessions),
{ timezone: TIMEZONE }
);
// Every hour: subscription expiry check
cron.schedule(
'0 * * * *',
() => runWithLock('subscription-check', 10 * 60 * 1000, checkSubscriptionExpiry),
{ timezone: TIMEZONE }
);
// Every minute: send notifications
cron.schedule(
'* * * * *',
() => runWithLock('send-notifications', 50 * 1000, sendPendingNotifications),
{ timezone: TIMEZONE }
);
logger.info('Cron jobs started');
}
BullMQ Repeatable Jobs Alternative
// src/jobs/bullmqCron.ts
export async function registerRepeatableJobs(): Promise<void> {
await cronQueue.add(
'cleanup-sessions',
{},
{
repeat: { pattern: '0 3 * * *', tz: 'Asia/Tokyo' },
jobId: 'cleanup-sessions', // Prevent duplicate registration
}
);
await cronQueue.add(
'subscription-check',
{},
{
repeat: { pattern: '0 * * * *', tz: 'Asia/Tokyo' },
jobId: 'subscription-check',
}
);
}
const worker = new Worker(
'cron-jobs',
async (job) => {
switch (job.name) {
case 'cleanup-sessions': return cleanupExpiredSessions();
case 'subscription-check': return checkSubscriptionExpiry();
}
},
{ connection: redisConnection, concurrency: 1 } // Serialize execution
);
Prisma Schema for Job History
model CronJobRun {
id String @id @default(cuid())
jobName String
status String // 'success' | 'failed'
startedAt DateTime
finishedAt DateTime
error String?
@@index([jobName, startedAt])
}
Summary
Design cron jobs with Claude Code:
- CLAUDE.md — Distributed lock required, error handling, explicit timezone
- Redis NX+EX lock — Prevent double-run across multiple servers
- DB execution history — Make incident investigation easy
- BullMQ repeatable — Simplify scheduler management at scale
Code Review Pack (¥980) includes /code-review to detect cron issues — missing locks, timezone bugs, missing failure alerts.
Myouga (@myougatheaxo) — Claude Code engineer focused on background job reliability.
Top comments (0)