Stop polling: move Jenkins from pollSCM to webhooks
pollSCM asks your Git server "anything new?" on a timer. Webhooks let the Git server answer the moment there is. Here is how to migrate, and when polling is still the right call.
Written by Loris Siegenthaler · Maker of BuildCaptainAlmost every Jenkins instance starts the same way. Someone creates a job, ticks Poll SCM, types * * * * * because they want builds to be fast, and moves on. Two years later there are four hundred jobs doing the same thing, the Git server's access log is 95% Jenkins, and builds still take up to a minute to start.
Polling is not wrong. It is just the fallback, and most teams are running it as the default. This is what it actually does, what it costs, and how to move the jobs that do not need it onto webhooks.
What pollSCM actually does
The pollSCM trigger schedules a periodic check against the repository configured on the job. On each run Jenkins asks the SCM whether the revision it last built is still the tip of the branch. If it is not, a build is queued. If it is, nothing happens and the poll is recorded in the job's Git Polling Log.
In a declarative pipeline it looks like this:
pipeline {
agent any
triggers {
pollSCM('H/5 * * * *')
}
stages {
stage('Build') {
steps {
sh './gradlew build'
}
}
}
}
triggers block in a Jenkinsfile only takes effect once Jenkins has read that file at least once — which means running the job manually the first time. The trigger is configuration that lives in the repo, and Jenkins cannot know about it until it has checked out the repo.Two flavours of polling exist, and they are not equally expensive. The Git plugin can usually answer "has the tip moved?" with a single git ls-remote against the remote — no workspace, no clone, no agent. But if the job's SCM configuration needs a workspace to decide (certain changelog/path-exclusion setups, some other SCM plugins), Jenkins has to occupy an executor and touch a real checkout on every poll. That is the version that hurts.
The cron syntax, and why H matters
Jenkins uses five fields — minute, hour, day of month, month, day of week — and adds one character that plain cron does not have: H.
H means "hash". Jenkins hashes the job's name into a stable value inside the allowed range, so every job gets its own offset and stays there. It is the difference between four hundred jobs all polling at 00 seconds past the minute and four hundred jobs spread evenly across the interval.
# Every five minutes, at a per-job offset inside each five-minute window
H/5 * * * *
# Once an hour, at a per-job minute
H * * * *
# Once a night, some time between 02:00 and 04:59
H H(2-4) * * *
# Twice a day, on weekdays only
H H(8-9),H(16-17) * * 1-5
Never write * * * * *. It means "every minute", it stacks every job on the same tick, and the job configuration page will warn you about it. If you genuinely need minute-level reaction time, that is the clearest possible sign that you want a webhook, not a faster poll.
The same syntax drives the cron trigger (build on a schedule regardless of changes) and the multibranch Scan Repository Triggers interval, so it is worth learning once.
What polling costs you
Three separate bills, and teams usually only notice the third.
Load on the SCM server. One job polling every five minutes is 288 requests a day. Four hundred jobs is 115,000 requests a day against your Git host, essentially all of them answering "no, nothing changed". Self-hosted GitLab or Bitbucket instances feel this directly. Hosted providers have rate limits that authenticated polling eats into, which is how teams discover their Jenkins is the reason their API quota is gone by lunchtime.
Load on the controller. Polling runs on the Jenkins controller, in a bounded thread pool. When polls start taking longer than expected — a slow remote, a repository that needs a workspace, a network hiccup — the pool backs up and Jenkins warns you that polling threads are starved. At that point polls are running late, which means builds are running late, which means the thing polling was supposed to buy you is exactly what you have lost.
Latency, always. With a five-minute interval the average wait between a push and a build starting is two and a half minutes, and the worst case is five. That delay is invisible on a dashboard and extremely visible to the developer who pushed a fix and is sitting there watching nothing happen. The Git plugin's own documentation is direct about the fix: to minimise the delay between a push and a build, configure the remote repository to notify Jenkins with a webhook.
Webhooks with GitHub
The GitHub plugin exposes a single endpoint on your Jenkins for every repository:
https://jenkins.example.com/github-webhook/
The trailing slash matters. In the GitHub repository, go to Settings → Webhooks → Add webhook and set:
- Payload URL — the URL above.
- Content type —
application/json. The plugin acceptsapplication/x-www-form-urlencodedtoo, but JSON is the conventional choice and the one every example uses. - Secret — a shared secret, if you have configured one on the Jenkins side. Set it; an unauthenticated build trigger is an invitation.
- Events — "Just the push event" for a plain job; add pull requests for multibranch.
On the Jenkins job, tick GitHub hook trigger for GITScm polling. The name is honest about what happens: the hook does not build blindly, it tells Jenkins to poll right now. Jenkins confirms the change against the repository and then builds. You keep polling's correctness and drop its schedule.
In a declarative pipeline:
pipeline {
agent any
triggers {
githubPush()
}
stages { /* … */ }
}
For multibranch pipelines and organization folders, the GitHub Branch Source plugin uses the same /github-webhook/ endpoint, but the effect is different: a push event triggers a scan of the affected branch instead of a build of a fixed job. That is what makes new branches and new pull requests appear in Jenkins within seconds instead of at the next periodic scan. Configure the webhook once at the GitHub organization level and every repository in it is covered.
Webhooks with GitLab
The GitLab plugin works per project rather than through one global endpoint. Each job gets its own URL:
https://jenkins.example.com/project/<job-name>
Jobs inside folders include the folder path — /project/platform/api-service. The job's configuration page shows the exact URL next to the Build when a change is pushed to GitLab checkbox, along with a Generate button for the secret token. Copy both into Settings → Webhooks on the GitLab project: URL in the URL field, token in Secret token. Multibranch projects use the same URL form.
Do not be tempted to point the GitLab webhook at /job/<name>/build instead. The plugin's own README is explicit that this bypasses it completely — you lose the branch and merge-request data from the payload, and with it every filter that depends on it.
pipeline {
agent any
triggers {
gitlab(
triggerOnPush: true,
triggerOnMergeRequest: true,
branchFilterType: 'All'
)
}
stages { /* … */ }
}
If you are on something that is neither GitHub nor GitLab — Gitea, a self-hosted mirror, an internal tool that pushes tags — the Generic Webhook Trigger plugin gives you a token-protected endpoint at /generic-webhook-trigger/invoke?token=… and lets you bind values out of the JSON payload to build parameters. For multibranch projects, the Multibranch Scan Webhook Trigger plugin does the same for scans at /multibranch-webhook-trigger/invoke?token=… — and accepts the token as a header instead of a query parameter, which keeps it out of access logs.
When polling is still the right answer
Webhooks require one thing that not every Jenkins has: a route from the SCM server to Jenkins. Keep polling when
- Jenkins sits behind a corporate firewall or on a private network with no inbound path from your Git host, and punching a hole for it is not a trade you want to make;
- you do not administer the repository and cannot add webhooks to it;
- the source is not a webhook-capable system at all — a vendor's SVN server, an artifact mirror, a filesystem drop.
In those cases poll deliberately rather than by accident: H/5 * * * * for something genuinely interactive, H/30 * * * * or H * * * * for everything else, and lightweight ls-remote-style polling wherever the SCM plugin supports it. Polling as a considered fallback with a sane interval is fine engineering. Polling every minute on four hundred jobs because nobody revisited the default is not.
A hybrid is also legitimate and commonly the right end state: webhooks as the fast path, plus a slow poll — daily, or every few hours — as the backstop that catches events lost to a Jenkins restart or a webhook delivery failure.
Quiet period: the setting that stops build storms
Once triggers fire instantly, bursts become visible. A developer pushes three commits in twenty seconds, or a script pushes to five branches at once, and Jenkins queues a build for each.
The quiet period is the built-in answer. When a build is triggered, Jenkins holds it in the queue for a number of seconds; further triggers for the same job arriving during that window are folded into the pending build rather than queued behind it. The global default is five seconds (Manage Jenkins → System), and any job can override it under Advanced — or in a pipeline with options { quietPeriod(30) }.
Two things are worth knowing. The window is not extended by later triggers: a build that has been waiting four seconds still starts one second later, however many pushes arrive in the meantime. And five seconds is a reasonable default for polling but often too short once you are on webhooks. If your team pushes in flurries, 30 to 60 seconds on the expensive jobs turns five redundant builds into one, and the only cost is half a minute of latency nobody was going to notice.
Fast triggers deserve fast feedback
Here is what changes after the migration. Under polling, the gap between pushing and knowing was dominated by the trigger — you pushed, you went to get coffee, you came back and checked Jenkins. With webhooks the build starts in seconds, and the slowest part of the loop is now you: the browser tab you have to remember to check, the dashboard you refresh, the Slack channel where the notification arrives among two hundred others.
Which is the problem we built our own tool for, so treat this as the disclosure it is. BuildCaptain is our app — a native macOS menu bar client for Jenkins. Pin the pipelines you care about and their status sits in the menu bar; when one turns red you get a native notification, and one click opens the stage graph, the failing step's log, and a rebuild button. You have just made Jenkins react to your pushes instantly. This is the other half of that loop.
BuildCaptain