DEV Community

myougaTheAxo
myougaTheAxo

Posted on

Cron Jobs with Claude Code: Distributed Locking, History Tracking, and Failure Alerts

"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)
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode
// 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');
}
Enter fullscreen mode Exit fullscreen mode

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
);
Enter fullscreen mode Exit fullscreen mode

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])
}
Enter fullscreen mode Exit fullscreen mode

Summary

Design cron jobs with Claude Code:

  1. CLAUDE.md — Distributed lock required, error handling, explicit timezone
  2. Redis NX+EX lock — Prevent double-run across multiple servers
  3. DB execution history — Make incident investigation easy
  4. 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.

👉 prompt-works.jp

Myouga (@myougatheaxo) — Claude Code engineer focused on background job reliability.

Top comments (0)