Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild

Jenkins Build Triggers: Stop Polling and Start Using Webhooks

Sean

Platform Writer

Aug 13, 2026
8 min read

Jenkins offers five ways to start a build: a webhook from your Git host, SCM polling, a cron schedule, an upstream job completing, and a remote trigger URL. Most setups use Poll SCM because it is the easiest to configure, and most should not — polling asks “has anything changed?” on a timer whether or not anything has, while a webhook tells Jenkins the moment a push happens. A workflow that reacts to an event should not sit there refreshing.

Jenkins Build Triggers: Stop Polling and Start Using Webhooks

The configuration for each is short. The differences that matter are latency, load, and whether the trigger works at all when Jenkins is not publicly reachable.

Table of contents

Webhooks, which should be the default

The Git host posts to Jenkins on push. Builds start in seconds and there is no repeated polling of anything.

pipeline {
    agent any
    triggers {
        // GitHub push events
        githubPush()
    }
    stages {
        stage('Build') {
            steps { sh 'make build' }
        }
    }
}

Two halves have to line up: the job must declare the trigger, and the Git host must be configured to post to https://jenkins.example.com/github-webhook/ — trailing slash included, since without it the endpoint does not match.

For a multibranch pipeline the trigger is on the scan rather than the job, and scan by webhook avoids re-scanning every branch on a schedule.

The catch: Jenkins has to be reachable from the internet. For an internal instance the options are an allowlisted ingress for the provider’s webhook addresses, a self-hosted runner arrangement, or falling back to polling. That constraint is why polling persists despite being worse.

Poll SCM, and how to make it less wasteful

triggers {
    // Every five minutes
    pollSCM('H/5 * * * *')
}

Jenkins asks the repository whether anything changed and builds if so. Simple, works behind a firewall, and wasteful — a five-minute interval across fifty jobs is 14,400 requests a day to find out that mostly nothing happened.

Use H rather than a fixed value. H/5 * * * * spreads jobs across the interval by hashing the job name; */5 * * * * fires every job at the same instant, producing a load spike every five minutes and a queue that never drains evenly.

The other cost is latency. Average wait is half the interval, so a five-minute poll means an average of two and a half minutes before a build starts — per commit, all day.

If you must poll, poll less often on quiet repositories and use webhooks wherever the network allows.

Scheduled builds

triggers {
    // Nightly between 2am and 3am, spread by job name
    cron('H 2 * * *')
}

For work that should happen on a schedule regardless of commits — nightly integration tests, dependency scans, cleanup, report generation.

Jenkins cron has a useful extension: H means “hash to a stable value in the valid range for this field”. H 2 * * * runs once between 02:00 and 02:59, at a consistent minute for that job. It avoids every nightly job in the organisation starting at exactly 02:00.

  • H/15 * * * * — every 15 minutes, offset per job
  • H 2 * * * — daily, between 02:00 and 02:59
  • H 2 * * 1-5 — weekdays only
  • H H(0-7) * * * — daily at some point between midnight and 08:00
  • @daily, @weekly, @midnight — aliases, also hashed

There is also cronWithTimezone when the schedule needs to follow a particular timezone rather than the controller’s, which matters more than people expect once daylight saving moves.

Upstream jobs and remote triggers

triggers {
    // Run after these jobs succeed
    upstream(upstreamProjects: 'build-library,build-common',
             threshold: hudson.model.Result.SUCCESS)
}

upstream is configured on the downstream job, which is usually the better direction — the job that cares about the dependency declares it, rather than the upstream job having to know about everything that follows it.

For triggering from outside Jenkins entirely — a deployment tool, another CI system, a manual script:

# Requires a token configured on the job
curl -X POST \
  "https://jenkins.example.com/job/deploy/build?token=SECRET_TOKEN"

# With parameters
curl -X POST \
  "https://jenkins.example.com/job/deploy/buildWithParameters?token=SECRET_TOKEN&ENVIRONMENT=staging"

# Using an API token instead, which is the better practice
curl -X POST -u "user:API_TOKEN" \
  "https://jenkins.example.com/job/deploy/build"

# With CSRF protection enabled, fetch a crumb first
CRUMB=$(curl -s -u "user:API_TOKEN" \
  'https://jenkins.example.com/crumbIssuer/api/xml?xpath=concat(//crumbRequestField,":",//crumb)')
curl -X POST -u "user:API_TOKEN" -H "$CRUMB" \
  "https://jenkins.example.com/job/deploy/build"

The crumb step is the one people hit and misdiagnose — a 403 on an otherwise correct request is usually CSRF protection rather than authentication. Prefer API tokens over build tokens in URLs, since a URL token ends up in shell history and logs.

Filtering what triggers a build

Not every push should build. A README change rebuilding a twenty-minute pipeline is pure waste.

pipeline {
    agent any
    stages {
        stage('Build') {
            when {
                anyOf {
                    changeset 'src/**'
                    changeset 'Dockerfile'
                    changeset 'package*.json'
                }
            }
            steps { sh 'make build' }
        }
    }
}

when { changeset } skips the stage rather than the build, so Jenkins still starts a job — that is the limitation. To avoid triggering at all, use the Git plugin’s included and excluded region settings, which filter before the build is queued.

// Only build for specific branches
when {
    anyOf {
        branch 'main'
        branch pattern: 'release/.*', comparator: 'REGEXP'
    }
}

Also worth setting: disableConcurrentBuilds() for deploy jobs, so two pushes in quick succession do not race each other into the same environment. That race produces the confusing case where the older commit wins because its build happened to finish last.

What all of this is for

The end state everyone is building toward is the same: a push produces a build, the build produces an artefact, and the artefact reaches an environment — with visibility at each step and a way back when it goes wrong.

Jenkins gets you there and is enormously flexible, which is both its strength and its cost. The triggers, the agents, the plugin versions, and the controller itself are all things you now maintain, and the controller is a single point of failure that needs its own backups and upgrades.

For a straightforward web service the question worth asking is whether that flexibility is being used. If the pipeline is “on push to main, build the repo and deploy it”, that is a capability rather than a project — building from a connected GitHub repository, with a build log per attempt, a live route, and the previous deploy retained for rollback, is the default shape on RunxBuild rather than something to configure and then maintain.

Where Jenkins earns its place is the genuinely complex case: matrix builds across platforms, hardware in the loop, orchestration across many repositories, or compliance requirements that demand a controller you own. Those are real, and they are not most projects.

How this fits the rest of the stack

Use webhooks where Jenkins is reachable — builds start in seconds and nothing polls. Fall back to pollSCM only behind a firewall, and always with H rather than a fixed minute so jobs spread across the interval. cron('H 2 * * *') for scheduled work, upstream declared on the downstream job, and API tokens with a CSRF crumb for remote triggers.

Filter with included and excluded regions rather than when { changeset } if you want to avoid queueing the build at all, and set disableConcurrentBuilds() on anything that deploys. If the pipeline is just build-and-deploy, the RunxBuild hosting calculator shows what that service and its database cost without a controller to maintain.

Useful related references:

FAQ

What is the difference between Poll SCM and a webhook in Jenkins?

Poll SCM asks the repository on a timer whether anything changed, so it wastes requests when nothing has and adds latency averaging half the interval. A webhook is pushed by the Git host the moment a commit lands, so builds start in seconds. Webhooks require Jenkins to be reachable from the internet, which is why polling persists.

What does H mean in a Jenkins cron schedule?

It hashes the job name to pick a stable value within the valid range for that field. H/5 * * * * spreads jobs across the five-minute window instead of firing them all simultaneously, and H 2 * * * runs once between 02:00 and 02:59 at a consistent minute for that job. Always prefer it to a fixed value.

How do I trigger a Jenkins build from a script?

POST to the job’s build URL with authentication — curl -X POST -u user:API_TOKEN https://jenkins.example.com/job/name/build. Use buildWithParameters with query parameters for a parameterised job. If CSRF protection is enabled, fetch a crumb from /crumbIssuer first and pass it as a header, or the request returns 403.

How do I stop Jenkins building on every commit?

Use the Git plugin’s included and excluded region settings to filter paths before the build is queued. The pipeline when { changeset 'src/**' } condition skips the stage but still starts a job, so it saves execution time rather than preventing the trigger entirely.

How do I trigger a Jenkins job after another job finishes?

Add an upstream trigger to the downstream job listing the upstream projects and a result threshold. Configuring it on the downstream side is preferable to post-build actions on the upstream job, because the job that depends on something declares its own dependency.

#jenkins#build triggers#ci cd#webhook#pipeline