CLAUDE LABJP
MODEL — Claude Sonnet 5 becomes the default across all plans, with stronger planning, tool use, and autonomyPRICE — Sonnet 5 launches at $2 input / $10 output per million tokens through August 31MODEL — Sonnet 5 nears Opus 4.8 performance at a lower price for always-on agentsCODE — Claude Code adopts Sonnet 5 as default with a native 1M-token context windowCODE — Claude Code adds sandbox credential blocking and org-level model restrictionsCLOUD — Claude is generally available in Microsoft Foundry on Azure with Azure-native accessMODEL — Claude Sonnet 5 becomes the default across all plans, with stronger planning, tool use, and autonomyPRICE — Sonnet 5 launches at $2 input / $10 output per million tokens through August 31MODEL — Sonnet 5 nears Opus 4.8 performance at a lower price for always-on agentsCODE — Claude Code adopts Sonnet 5 as default with a native 1M-token context windowCODE — Claude Code adds sandbox credential blocking and org-level model restrictionsCLOUD — Claude is generally available in Microsoft Foundry on Azure with Azure-native access
Articles/Cowork
Cowork/2026-07-02Advanced

How Many Tasks Fire in the Same Minute — Flattening Cowork Scheduled-Task Collisions from Cron

When Cowork scheduled tasks bunch up at the same time and fight over shared resources, you can expand every cron expression into fire times, count collisions and true concurrency, and shave the peak with a greedy offset that never moves your premium slots. With working code and measured before/after numbers.

Cowork32Scheduled Tasks9cron2Automation34Concurrency2

Premium Article

The other night, several tasks fired at exactly 20:00, fought over the free space in a shared /tmp and the rate-limit budget, and a later job ran out of breath halfway through a clone. As an indie developer I let Cowork scheduled tasks run the automated posting for several sites, and by adding one task at a time I had quietly drifted into a state where a handful of them all fired at the same minute at night.

The tricky part is that each task looks perfectly healthy. Every cron is doing exactly what it was told. The problem lives in a property you can only see from above: how many jobs start in a single minute once you line them all up. This piece pulls that overhead view out of the cron expressions mechanically instead of relying on intuition, and shaves the peak while leaving the revenue-critical premium slots completely untouched.

Expand cron into hours and minutes, then count same-minute fires

The first thing we need is a full expansion of "which task fires at which minute of the day." A Cowork scheduled task is described by a five-field cron (minute hour day month weekday), so we pull the fire minute (a 0–1439 index) out of it.

Real-world crons are almost always plain numbers or comma lists (for example 0 9,20 * * *), so starting there cuts down on parsing mistakes. Adding ranges and steps is fine once this foundation works correctly.

type Task = { id: string; cron: string; durationMin: number; protected: boolean };
 
// Expand a field like "0,30" or "9,20" into an array of numbers
function parseField(field: string, min: number, max: number): number[] {
  if (field === "*") {
    const all: number[] = [];
    for (let v = min; v <= max; v++) all.push(v);
    return all;
  }
  return field.split(",").map((s) => {
    const n = Number(s);
    if (!Number.isInteger(n) || n < min || n > max) {
      throw new Error(`Invalid cron field value: "${s}"`);
    }
    return n;
  });
}
 
// Return the day's fire times (minute index 0..1439). We ignore day/month/weekday
// here on the assumption of "runs daily", and look at daily concurrency using only
// hour and minute (most generation tasks run daily or on fixed weekdays).
function fireMinutes(cron: string): number[] {
  const parts = cron.trim().split(/\s+/);
  if (parts.length !== 5) {
    throw new Error(`cron does not have five fields: "${cron}"`);
  }
  const [mField, hField] = parts;
  const minutes = parseField(mField, 0, 59);
  const hours = parseField(hField, 0, 23);
  const out: number[] = [];
  for (const h of hours) for (const m of minutes) out.push(h * 60 + m);
  return [...new Set(out)].sort((a, b) => a - b);
}

With each task's fire minutes in hand, we build a map of "how many fire in the same minute."

// fire minute -> the task IDs that start in that minute
function collisionMap(tasks: Task[]): Map<number, string[]> {
  const map = new Map<number, string[]>();
  for (const t of tasks) {
    for (const fm of fireMinutes(t.cron)) {
      const list = map.get(fm) ?? [];
      list.push(t.id);
      map.set(fm, list);
    }
  }
  return map;
}
 
function toHHMM(minute: number): string {
  const h = String(Math.floor(minute / 60)).padStart(2, "0");
  const m = String(minute % 60).padStart(2, "0");
  return `${h}:${m}`;
}

Pick out the minutes in collisionMap whose value length is 2 or more, and you get a list of "tasks that fire in the same minute." My first mistake here was assuming this "same-minute count" was the concurrency itself. It is only the entrance.

"Overlapping starts" and "how many run at once" are not the same

Even when starts land in different minutes, tasks with long run times overlap while running. A job that starts at 19:45 and takes 20 minutes runs alongside another that starts at 20:00, together from 20:00 to 20:05. So what we really want is not "collision of starts" but "how many are running at a given instant" — the true concurrency.

We lay each run out as an interval [fireMinute, fireMinute + durationMin) and find the maximum concurrency with a sweep line. There is one easy-to-miss trap. When an "end" and a "start" happen in the same minute, counting them as overlapping overstates concurrency — you misdetect two merely adjacent jobs as concurrent.

type Interval = { start: number; end: number; id: string };
 
function runIntervals(tasks: Task[]): Interval[] {
  const intervals: Interval[] = [];
  for (const t of tasks) {
    for (const fm of fireMinutes(t.cron)) {
      intervals.push({ start: fm, end: fm + t.durationMin, id: t.id });
    }
  }
  return intervals;
}
 
function peakConcurrency(intervals: Interval[]): { peak: number; atMinute: number } {
  const events: { minute: number; delta: number }[] = [];
  for (const iv of intervals) {
    events.push({ minute: iv.start, delta: +1 });
    events.push({ minute: iv.end, delta: -1 });
  }
  // In the same minute, process the end (-1) before the start (+1).
  // Otherwise a job "ending at 20:05" and one "starting at 20:05" get
  // counted as running together.
  events.sort((a, b) => a.minute - b.minute || a.delta - b.delta);
 
  let cur = 0;
  let peak = 0;
  let atMinute = 0;
  for (const e of events) {
    cur += e.delta;
    if (cur > peak) {
      peak = cur;
      atMinute = e.minute;
    }
  }
  return { peak, atMinute };
}

The key is that sorting delta ascending puts -1 ahead of +1. This lines up with treating intervals as half-open [start, end), so adjacent jobs are not counted as concurrent. Get this one ordering wrong and the peak looks one or two jobs higher than reality, which pushes you to move tasks you didn't need to touch.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
A TypeScript implementation that expands every task's cron into a day of fire times and detects both same-minute collisions and per-window concurrency
A sweep-line that separates 'overlapping starts' from 'how many run at once', including the ordering trick that stops adjacent runs being counted as concurrent
A greedy offset that keeps premium slots fixed and moves only the flexible tasks to cut the peak, with a measured before/after
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

Related Articles

Cowork2026-06-13
Running Cowork Scheduled Tasks in Practice — From a Morning Digest to Unattended Weekly Reports
How to set up recurring runs, reminders, and automated reports with Claude Cowork's scheduled tasks — covering cron basics, prompt design that survives unattended execution, and how to schedule multiple tasks so they stay reliable.
Cowork2026-07-01
Let the Downstream Task Verify the Upstream Actually Ran Today: A Completion Ledger and Dependency Barrier for Unattended Schedulers
Unattended schedulers have no notion of dependencies, so when a morning data-refresh task fails silently, the noon generation task keeps running on yesterday's leftovers. This is a design for recording upstream completion atomically and having downstream assert its preconditions before running, with working TypeScript and lessons from my own operations.
Cowork2026-06-29
Failing Loud on Stale Inputs: A Freshness Contract for Unattended Pipelines
How to stop a scheduled, unattended pipeline from silently shipping degraded work when its upstream data is empty or stale. We implement a freshness contract in bash that asserts recency, non-emptiness, and provenance, plus two real pitfalls I hit running Cowork scheduled tasks.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →