The message means registration worked but the runner never called home. That narrows the problem to five things: the process isn't running, it's reading a different config file, it can't reach your URL, TLS is failing, or the token belongs to someone else, including the case where gitlab-runner verify reports 'is valid' and GitLab still says the runner has never contacted it.
You created a runner, GitLab listed it, and next to it sits a grey warning: This runner has never contacted this instance. Your pipeline sits pending, and nothing in the UI tells you why.
The good news is that this message is far more specific than it looks. It rules out most of what you might be about to check.
GitLab runners poll. The runner process asks GitLab for work every few seconds
(check_interval, three seconds by default). GitLab never pushes anything to
the runner, and it never opens a connection to it.
So this warning means exactly one thing: a runner record exists in GitLab's database, and no runner process has ever polled it.
That splits the problem cleanly in two:
Every cause below is a variation on the second half. Notably, none of them are
about your .gitlab-ci.yml, your tags, or your job configuration. Those cause
different symptoms. Don't start there.
Since GitLab 16, you create a runner in the UI first, and it appears in the list
immediately with an authentication token starting with glrt-. The runner row
exists before any machine has been told about it.
This is by far the most common reason for the warning, and it is not a bug. The UI is showing you a runner you created but haven't installed. If you stopped after copying the token, that's the whole story.
Check before assuming anything more interesting:
sudo systemctl status gitlab-runner # package install
docker ps -a | grep gitlab-runner # container installA container that exited seconds after starting almost always means a malformed
config.toml. Read the logs rather than restarting it:
sudo journalctl -u gitlab-runner -n 50 --no-pager
docker logs gitlab-runnerThis one wastes the most time, because everything looks correct.
gitlab-runner register writes to a config file that depends on which user ran
it. The service reads a path that depends on how it was installed. When those
disagree, registration genuinely succeeded and the running service genuinely
never learned about it.
Confirm which file the runner actually loaded, and confirm your runner is in it:
sudo grep -c '\[\[runners\]\]' /etc/gitlab-runner/config.tomlZero means you registered into a different file. Re-register with sudo, or
point the service at the file you already wrote.
Registration and polling are the same API, so if registration worked from that
machine, the network path worked at least once. That makes this cause more
likely when someone else registered the runner, when you copied a config.toml
between hosts, or when the runner lives in a different network segment.
Test from the runner itself, not from your laptop:
curl -v https://gitlab.example.com/api/v4/versionFor self-managed instances, the usual failure is a URL that only resolves in
some networks. An internal hostname, a VPN-only address, or a private IP will
work from your desk and fail from a runner in a different subnet or a cloud
region. The url in config.toml has to be reachable from the runner, which
is not always the same address you type into your browser.
Also check outbound egress. Plenty of hardened networks allow inbound HTTPS to GitLab and quietly drop outbound traffic from build hosts.
Self-signed certificates and private CAs fail here constantly, and the error in the runner log is often a single line about certificate verification that scrolls past.
sudo gitlab-runner --debug runRun it in the foreground and watch. If you see a certificate error, give the runner the CA explicitly:
[[runners]]
url = "https://gitlab.example.com/"
tls-ca-file = "/etc/gitlab-runner/certs/ca.crt"The file has to be readable by the user the service runs as, which is often not the user that created it.
A few variations, all of which produce a runner record that never polls:
--url was passed during a non-interactive registration, so the runner
defaulted to https://gitlab.com/ and is contacting the wrong instanceThat last one is worth checking early on self-managed setups. Open
config.toml and read the url line before anything else.
gitlab-runner verify is the fastest single test. It performs the same
authenticated call that polling uses, and it prints the failure instead of
burying it in a log.
This is the combination that wastes the most time, because the one command everyone reaches for comes back clean:
Verifying runner... is valid runner=AbCdEfGhAnd the runners page still says the runner has never contacted the instance.
Nothing is lying to you. verify and polling are not the same test.
verify proves that a token in a config file is accepted by a GitLab that this
shell can reach. Polling additionally requires that a service is alive, that
it read the same config file, and that the runner is not paused. Verify
covers the credentials. It says nothing about the daemon.
So when both are true at once, check these, in this order:
sudo gitlab-runner verify works fine
with the service stopped, because you are the one making the call.
systemctl status gitlab-runner is the check that matters.sudo, verify reads /etc/gitlab-runner/config.toml. Run as your own user,
it reads ~/.gitlab-runner/config.toml. If you registered without sudo and
the service runs as root, you just verified a token the daemon has never
seen. sudo gitlab-runner --config /etc/gitlab-runner/config.toml verify
settles it.The general rule: verify failing tells you something useful. verify
passing only rules out causes 4 and 5, and sends you back to causes 1 and 2.
On GitLab.com, the URL is public, the certificate is valid, and only one side of the connection is yours. Most of this list collapses to "is the runner running."
On a self-managed instance, you own both ends. The reverse proxy in front of
GitLab, the external_url it was configured with, the certificate chain, the
DNS split between internal and external views, and the egress rules on the
build host are all yours to get right, and they only have to disagree slightly
before the runner goes quiet without a useful error.
That's the real cost of self-hosting, and it rarely shows up in the plan. The server is cheap. The afternoon spent proving that a runner can reach your own GitLab is not.
Work from the message outward. It already told you registration succeeded, so skip your CI config and check the five things above in order. Most of the time it's cause 0 or cause 2, and both are five-minute fixes once you stop looking at the pipeline.
If you'd rather not do this again, RocketRunner provisions the machine, installs the runner, and connects it to your GitLab for you, including self-managed instances. Dedicated, isolated, and destroyed when you delete it.
Start your free trial and skip the config.toml archaeology.
// Keep reading
// gitlab
Jul 30, 2026 · 6 min read
// gitlab
Aug 12, 2026 · 6 min read
// gitlab
Aug 11, 2026 · 5 min read