jenkins ci-cd load-distribution

Jenkins H Syntax: How Hash-Based Scheduling Prevents Thundering Herd

by Sinthuyan Arulselvam  · April 8, 2026

Every Jenkins installation eventually hits the same wall. You start with ten jobs, all scheduled at 0 * * * * because that's the intuitive choice — top of the hour, every hour. Then the team grows. Thirty jobs. Eighty. Suddenly your build agents are completely saturated for ninety seconds starting at exactly :00 and idle for the next fifty-eight minutes. Your Git server logs show a burst of connection resets. Your Nexus repository manager throws 503s. This is the thundering herd problem, and Jenkins' H syntax exists specifically to kill it.

The Thundering Herd Problem

The name comes from distributed systems literature, but the Jenkins manifestation is straightforward. When a large number of jobs share the same trigger time, they all wake up simultaneously and compete for the same finite pool of resources.

Consider a realistic scenario: an organisation runs 80 microservice pipelines, each scheduled at 0 9 * * 1-5 — 9 AM on weekdays. At 09:00:00 Monday morning, all 80 jobs enter the queue simultaneously. If the Jenkins instance has 20 build agents, 60 jobs are immediately queued. The first 20 pipelines all clone from the same Git host at once. If those pipelines share a Maven or npm dependency cache, the cache warm-up hits happen in parallel. If they all push Docker images, the registry is hammered in a tight burst.

The damage compounds in several ways:

  • Build agent saturation: All agents are fully occupied for minutes, then drop to idle. Provisioned capacity is sized for the burst, not the average, wasting infrastructure budget all day.
  • Git server connection limits: GitHub, Bitbucket, and self-hosted Gitea all enforce per-IP or per-user connection concurrency limits. 80 simultaneous clones from one Jenkins IP will hit those limits and produce intermittent authentication or timeout failures that are almost impossible to debug.
  • Dependency resolver saturation: Maven Central, npm registry, PyPI — all have rate limits. A burst of 80 dependency fetches in under a minute will trigger throttling, causing random build failures that blame the network, not the schedule.
  • False build failure correlation: When all jobs start together, a transient infrastructure hiccup affects all of them at once, making it look like a systemic outage rather than a scheduling artefact.

The fix sounds simple — just stagger the jobs. But manually assigning different minute offsets to 80 pipelines is tedious, error-prone, and breaks the moment someone adds a new job without checking the existing spread. You need a mechanism that distributes jobs automatically. Jenkins provides exactly that.

What H Does

The H token is a deterministic hash function applied to the job's full name. Jenkins takes the job path — something like folder/my-service/main — computes a hash, and uses that hash value as an offset within the valid range of the field where H appears.

For the minutes field (0–59), H resolves to some integer in [0, 59]. That integer is the same every time because the job name doesn't change. No randomness is involved at runtime — the value is computed once and fixed. Two different jobs with different names will almost certainly get different offsets, because their hashes differ. That's the distribution. One job will always resolve to the same offset, no matter how many times you reload Jenkins or restart the process. That's the stability.

Internally, Jenkins uses a simple CRC32 or similar hash of the job name modulo the field range. The implementation detail matters less than the property: it's a pure function of the name.

H Properties in Practice

Three properties define H's usefulness:

  • Deterministic: The same job always resolves H to the same value. You can predict and verify the actual trigger time by inspecting the job or its build history.
  • Distributed: Different jobs resolve to different values. A fleet of 60 hourly jobs using H * * * * will spread roughly evenly across the 60 minutes, rather than all firing at minute 0.
  • Stable across restarts: Because the hash is derived from the name, not from any runtime state, a Jenkins restart or plugin upgrade does not reshuffle your schedule. The job will trigger at the same time after a restart as before.

Basic H Patterns

These four patterns cover the majority of use cases:

bash
H * * * *       # Once per hour, at a hash-determined minute
H H * * *       # Once per day, at a hash-determined hour and minute
H H * * 0       # Once per week (Sunday), at a hash-determined time
H H 1,15 * *    # Twice per month, on the 1st and 15th

Hourly (H * * * *): The job runs once every hour, but not necessarily at :00. A job named api-gateway/build might resolve to minute 37, so it runs at 00:37, 01:37, 02:37, and so on. Spread 60 such jobs and they tile the hour perfectly.

Daily (H H * * *): Two H tokens — one for hour (0–23) and one for minute (0–59). The job runs once per day at a fixed but hash-derived time. Essential for nightly builds; if every team uses this pattern, no two nightly builds are likely to collide.

Weekly (H H * * 0): Runs once per week on Sunday. The hour and minute are still hash-derived, so 20 weekly jobs will scatter across Sunday rather than all starting at midnight.

H with Step Values

H/N means "run every N minutes (or hours), but start at a hash-determined offset rather than at zero." Without H, */15 * * * * runs at :00, :15, :30, :45. With H/15 * * * *, a job might run at :07, :22, :37, :52. Another job might run at :03, :18, :33, :48.

bash
H/15 * * * *    # Every ~15 minutes, hash-offset start
H/30 * * * *    # Every ~30 minutes, hash-offset start
H/2 H(9,17) * * 1-5   # Every 2 hours during business hours, weekdays only

This is the most impactful pattern for high-frequency polling jobs. If 40 pipelines all poll SCM every 15 minutes using */15 * * * *, they all hit the Git server at :00, :15, :30, and :45 — four thundering herds per hour. Switching to H/15 * * * * distributes those 40 polls across each 15-minute window, flattening the load curve almost completely.

Note that H/15 does not guarantee exactly 4 runs per hour in every case — the hash offset means the exact minutes depend on the job name. Jenkins guarantees at least one run per 15-minute window, not exactly at the window boundaries.

H with Range Constraints

H(min,max) constrains the hash to the given inclusive range. This is the tool for pinning jobs to a specific window without picking an exact minute.

bash
H(0,29) * * * *         # First half of every hour
H(30,59) * * * *        # Second half of every hour
H H(9,16) * * 1-5       # Business hours only (9 AM–4 PM), weekdays
H(0,29) H(22,23) * * *  # Nightly, between 10 PM and 11:29 PM

Business hours constraint (H H(9,16) * * 1-5): The job runs once per weekday, at a hash-determined minute within the 9 AM to 4 PM window. This is perfect for integration test pipelines that should not run overnight (where failures might be missed) but also should not all collide at 9:00 AM sharp.

Deployment windows (H(0,29) H(2,4) * * *): The job runs once per day, during the 2 AM to 4:29 AM maintenance window, at a hash-determined exact time. You get the distribution benefit without risking a deployment at 4:55 AM when the window is supposed to close at 5 AM.

When NOT to Use H

H is a heuristic for load distribution. It is explicitly wrong in these situations:

  • Financial cutoffs: End-of-day settlement jobs must run at a precise time. H 17 * * 1-5 is not acceptable if the job must complete before 17:30 and H might resolve to 17:51.
  • SLA-bound jobs: If a report must be delivered by 08:00, scheduling it with H H(6,7) * * * introduces non-determinism that will eventually violate the SLA.
  • Coordinated pipelines: If Pipeline B must start only after Pipeline A has completed, use Jenkins' upstream trigger (triggers { upstream(...) }) rather than trying to schedule them with a known H offset gap.
  • Exact compliance audit trails: Some regulated industries require evidence that a process ran at an exact specified time. H-derived times are not the specified time.

For anything where the exact minute matters, use a literal cron expression. H is for jobs where "roughly hourly" or "sometime overnight" is sufficient — which, in practice, is the majority of CI/CD work.

Predicting Your H Value

Because H is deterministic, you can always find out what time it resolves to without waiting for the job to run.

  • Jenkins UI tooltip: In the job configuration page, hover over the question mark next to the cron schedule field. Jenkins will display the computed schedule in plain English, e.g. "would last have run at Thursday, June 12, 2026 14:37:00; would next run at Friday, June 13, 2026 14:37:00".
  • Build history: Look at the timestamps of recent builds. If the job uses H * * * * and you see builds at :22 past every hour, your H value for the minutes field is 22.
  • Jenkins script console: You can evaluate the schedule programmatically using the Groovy console to inspect the trigger configuration.

Understanding the resolved time is important when you need to reason about pipeline sequencing or when documenting the expected run window for stakeholders.

H in Declarative vs Scripted Pipeline

In a Declarative Pipeline, cron triggers live in the triggers block:

groovy
pipeline {
  agent any
  triggers {
    cron("H/15 * * * *")
  }
  stages {
    stage("Build") {
      steps {
        sh "make build"
      }
    }
  }
}

In a Scripted Pipeline, use the properties step:

groovy
properties([
  pipelineTriggers([
    cron("H H(2,4) * * *")
  ])
])

One critical nuance: the hash is computed from the job's full path within Jenkins, not just the job name. A job at team-a/service/main and a job at team-b/service/main will get different H offsets despite having the same leaf name. This is intentional — it means moving a job to a different folder changes its hash and therefore its schedule. If you are migrating jobs between folders and need to preserve run timing, verify the new resolved time before pushing the change.

This also means that two jobs with identical Jenkinsfile contents but different paths will run at different times. The folder-path-as-hash-input behaviour is a feature: it provides additional natural distribution across an organisation's folder hierarchy.

Comparison with Other Load Distribution Approaches

Mechanism How It Works Deterministic? Requires Code Change? Granularity
Jenkins H Hash of job name used as cron field offset Yes — same job, same time always Only the Jenkinsfile trigger expression Minute-level
No distribution All jobs use literal 0 * * * * Yes — predictably bad N/A N/A — all fire together
Manual staggering Ops assigns each job a different minute offset by hand Yes, but fragile Every new job requires coordination Minute-level
Kubernetes jitter (initialDelaySeconds randomisation) Random sleep injected at pod start No — different offset each restart Yes — pod spec or init container Second-level
systemd RandomizedDelaySec Uniform random delay added to each service start No — random per boot Yes — systemd unit file Second-level
Celery jitter on beat schedule Random seconds added to each task invocation No — random each time Yes — task definition Second-level

Jenkins H's key differentiator is that it is deterministic and automatic. systemd's RandomizedDelaySec and Kubernetes init-container jitter both introduce true randomness — which means the job can run at a different time after every restart, making it impossible to reason about scheduling without watching logs. They also require changes to infrastructure configuration rather than just the pipeline definition.

Manual staggering works at small scale but has a maintenance cost that grows quadratically with team size. Every new job requires someone to look at the current spread and pick an unused slot. H handles this automatically — adding the 81st job to a fleet of 80 requires no coordination; it gets its own hash-derived slot for free.

The honest limitation of H compared to Kubernetes-level approaches is granularity. H operates at minute-level cron resolution. If you need sub-minute distribution (for example, 500 jobs that all need to start within a 30-second window but not at the exact same second), H cannot help — you would need a queue-based approach or randomised delays at the execution layer. For the 99% of Jenkins use cases involving minute-scale schedules, H is the right tool.

Putting It Together: A Migration Checklist

If you are converting an existing Jenkins installation from literal cron times to H-based scheduling:

  • Audit all jobs currently using 0 * * * *, 0 2 * * *, or other round-number times — these are the thundering herd candidates.
  • For non-time-sensitive CI jobs, replace literal minutes with H. Replace 0 * * * * with H * * * *.
  • For jobs that need a general window but not a precise time, use range constraints: H H(1,4) * * * for "sometime between 1 and 4 AM".
  • Leave financial, SLA, or coordinated jobs on literal cron expressions. Document why.
  • After deploying, verify the resolved times via the UI tooltip or the first build's timestamp. Confirm with stakeholders if the new run window is acceptable.
  • For high-frequency polling jobs (*/5, */15), switch to H/5 and H/15 respectively — this is often the highest-impact single change you can make to a busy Jenkins instance.

The thundering herd is not an exotic edge case. It is the default outcome of the most natural scheduling intuition — round numbers, top of the hour. Jenkins' H syntax is the one-line fix that requires no infrastructure changes, no coordination overhead, and no ongoing maintenance. It should be the default choice for every CI/CD trigger that does not have a hard timing requirement. If your Jenkinsfiles still use 0 * * * *, changing it to H * * * * is one of the highest-leverage, lowest-risk improvements you can make today.

Explore related resources

Related Guides