GitHub Actions vs Concourse CI: pushed at, or polling
GitHub Actions vs Concourse comes down to who initiates: a GitHub Actions run is dispatched when a repository event arrives, while a Concourse pipeline is a long-lived object on a server that continuously checks its resources for new versions, every minute by default. Everything else that differs, from how a branch gets a pipeline to how many machines you operate, follows from that one choice.

Concourse describes itself as a pipeline-based continuous thing-doer, and is opinionated in a way its own documentation states up front: idempotency, immutability, declarative config, stateless workers and reproducible builds. Everything in Concourse runs in a container, and a task defines the image it needs rather than relying on what somebody installed on a worker.
GitHub Actions is the opposite kind of system by design. A workflow file lives in a branch, it is discovered automatically, and the run is triggered by an event GitHub already knows about. Nothing polls, and nothing has to be told a branch exists.
Everything below was read from the concourse/docs repository and the concourse/concourse README on 21 September 2026, against Concourse v8.3.0, and from GitHub's own references. Concourse is Apache 2.0 and free; the operating cost is three components, and that is what this page prices.
GitHub Actions against Concourse CI, by mechanism (read 21 September 2026)
| GitHub Actions | Concourse CI | |
|---|---|---|
| How a run starts | A repository event is dispatched to a runner whose labels match | A resource check finds a new version, and the pipeline queues a build for any job whose inputs changed |
| Detection latency | Webhook time. The event is already inside GitHub | check_every defaults to 1m per resource, with a check_timeout default of 1h. Per-resource webhooks are available with a webhook_token |
| How config gets there | Commit a file to .github/workflows. A new branch brings its own workflows with it | fly set-pipeline, or a set_pipeline step inside another pipeline. A new branch does not get a pipeline by existing |
| What you operate | Nothing, or self-hosted runners if you choose | A web node, a worker node and a PostgreSQL node, plus the fly CLI for every operator |
| What you pay per minute | $0.006 at 2 vCPU on Linux, or $0.0025 on a managed runner at the same size | Nothing. Apache 2.0 software on hardware you pay for whether or not a build is running |
| Extensibility model | Actions from the marketplace, pinned by tag or commit SHA | Resource types declared inside the pipeline itself, which the docs describe as keeping Concourse lean without needing a complex plugin system |
| Build environment | A runner image GitHub maintains, with common toolchains preinstalled | Every task names its own container image, so nothing is preinstalled and nothing drifts |
| Access control | Repository and organization permissions you already have | Concourse teams, with local users or GitHub, GitLab, LDAP, OIDC, SAML and several other auth backends to wire up |
| Self-healing or retries | None on GitHub-hosted runners; transient failures diagnosed and retried inside the run on Latchkey | None for a failed build. The scheduler will queue a new build when a new version appears, which is a different thing |
| Setup change required | A file in the repository | Three nodes, a database, an auth backend, and a pipeline set per repository or per branch |
Resources, and why the model is worth the trouble
A Concourse resource represents a versioned artifact with an external source of truth: a git repository, an S3 object, an image in a registry, or the passage of time. Concourse continuously checks each configured resource to discover new versions, and those versions flow into jobs through get steps. Configuring the same resource in any pipeline on any Concourse cluster behaves the same way, which is the reproducibility claim the project is built on.
The payoff is real. Because a job's inputs are declared versions rather than whatever happened to be on disk, a build that passed can be replayed with the same inputs, and a pipeline is a dependency graph rather than a sequence of scripts that mostly works. Teams that have been burned by a CI server whose agents accumulated state tend to recognise the value of this immediately.
The price is a learning curve, and Concourse does not pretend otherwise. Its documentation says outright that Concourse admittedly has a steeper learning curve initially, and asks you to keep going on the promise that it flattens. That is an honest framing, and it is also the single biggest reason adoption stalls on teams that tried it for one repository.
The branch problem
This is where the two models diverge most sharply in daily use. A GitHub Actions workflow is a file in a branch, so a new branch arrives with its workflows already attached and on: pull_request does what it says. A Concourse pipeline is an object on the server, configured by fly set-pipeline or by a set_pipeline step, and once configured the docs say it takes on a life of its own, continuously detecting resource versions and queueing builds.
Nothing about that is broken, but it means something has to set a pipeline when a branch appears and remove it when the branch is merged. Concourse has answers, including instanced pipelines and the set_pipeline step that lets one pipeline configure others, and teams that run Concourse at scale use them. The point for a reader choosing between the two is simply that this is work on one side and not on the other.
The polling default matters here too. With check_every at its default of one minute, a push can wait up to a minute before anything notices, per resource. Per-resource webhooks exist and remove that delay where you configure them, though the docs note you cannot currently have webhooks for all instances of an instanced pipeline.
Speed
Not benchmarked yet, and the benchmark would need to separate two different quantities that get conflated in complaints about Concourse being slow: detection latency and execution time.
Detection is documented rather than measured, and it is the one Concourse loses by default at up to a minute per resource against a webhook. Execution is a property of the worker you provisioned, and there is no reason a container on your worker should be slower than the same container on a runner, other than the machine underneath it. If the comparison you are actually making is about how fast a build runs, the variable is the hardware, and that is the same question as choosing a runner size.
Cost: free software, three machines
Concourse costs nothing to license and is distributed as a single binary. The bill is a web node, a worker node and a PostgreSQL node, all running whether or not anything is building, plus whoever upgrades them. For a team with steady, high-volume CI that is cheap: hardware amortised across a full working day beats metered minutes at any vendor rate.
The comparison on the other side is one multiplication. Ten thousand Linux minutes a month is $60 at GitHub's $0.006 list rate for 2 vCPU, $42 on a Team plan after the 3,000 included minutes, or $25 at $0.0025 a minute on a managed runner at the same size. Set that against three always-on machines, a database to back up and an operator, and the answer usually depends on how much CI you actually run rather than on any preference about pipelines.
Why there is no recorded run on this page
The claim this page turns on is a default value and a mechanism, not a duration: check_every is documented as one minute, and a pipeline is configured out of band with fly. Standing up a web node, a worker and a database to watch a resource check would confirm a number the schema already publishes, and the wall clock of any build we ran would be a fact about our worker. The configuration schema and the README are linked below and take less time to read than the cluster takes to boot.
When each one is right
- Concourse when reproducibility is the requirement and you have been burned by stateful agents. Immutable workers and declared input versions are the design answer to that problem, not a workaround for it.
- Concourse when your automation is not really CI: cross-repository orchestration, scheduled infrastructure work, anything where the trigger is a new artifact version rather than a commit.
- Concourse when the code is not on GitHub. Actions is coupled to GitHub repositories and is not a general automation server.
- GitHub Actions when pull request checks are most of what CI does for you, because the branch, the check and the merge gate are one object and nothing has to be told a branch exists.
- GitHub Actions when there is nobody to run three nodes. That is not a slight on Concourse; it is the same calculation that decides self-hosted runners against managed ones.
The verdict
Concourse is a better-designed automation system than most of what it is compared to, and the resource model is the reason people who like it like it so much. It is also three machines, a database, an auth backend and a CLI on every operator's laptop, in exchange for a model whose own documentation warns you about the learning curve. If your pipelines are genuinely about artifact versions flowing between jobs, that trade is worth making and this page is not going to talk you out of it. If your pipelines are "run the tests when someone opens a pull request", Actions does that with a file in a branch and nothing to operate, and the money you were going to save on licensing is smaller than the cost of the person who owns the web node.
Frequently asked questions
What do I have to run to use Concourse?
fly CLI, which you download from the web UI. There are Docker Compose and Helm paths for getting started, but the components are the same underneath.Does Concourse use webhooks or polling?
check_every setting that defaults to one minute, and a check_timeout that defaults to one hour. You can add a per-resource webhook with a webhook_token and call the check endpoint directly, which removes the delay for that resource. GitHub Actions is event-dispatched and has no equivalent polling step.How do Concourse pipelines handle new branches?
fly set-pipeline or by a set_pipeline step in another pipeline, so a branch does not acquire a pipeline simply by existing the way a GitHub Actions workflow file does. Instanced pipelines and pipeline-setting pipelines are the supported patterns for automating it.