Troubleshooting
Most of the time «the bot is silent» is one of four breaks in the chain GitLab → webhook → bot → model. Run the diagnostics script — it finds which one — or walk the checks below by hand.
The diagnostics script
diagnose.sh runs on the host next to the bot's docker-compose and .env and checks the whole chain in a couple of seconds: the environment variables, the health of the bot, the GitLab token, webhook delivery, Redis, the model key and the proxy. It also prints the version of your bot — the thing support asks you to attach.
curl -O https://reviewgate.dev/diagnose.sh
less diagnose.sh # read it — it is short and holds no surprises
bash diagnose.sh.env (line by line, never executing it) and sends harmless requests: a GET to your GitLab and to the bot, a webhook «ping» that the bot ignores (no review starts), and GET /v1/models to the model — which spends no tokens. Neither the diff nor the code goes anywhere.Options for a non-standard setup:
# the bot is behind a reverse proxy, or the port is not on localhost:
bash diagnose.sh --bot-url https://bot.acme.ru
# and check that the webhook is configured in the project:
bash diagnose.sh --project 42No review appeared on the request
Five checks in the order of the chain — or simply run diagnose.sh:
- The bot is alive.
curl http://localhost:3000/api/healthmust returnstatus:ok. If it does not, see «The bot does not answer» below. - The webhook is delivered.GitLab → the project → Settings → Webhooks → your hook → Recent events. The response code of the last delivery is visible there:
202— the bot accepted it;401— the secret did not match; a connection error — GitLab could not reach it. - GitLab can reach the bot.A bot on an internal network needs an explicit permission — see «the local network» below.
- The secret matches.
GITLAB_WEBHOOK_SECRETagainst the Secret token field of the webhook — see «the webhook was rejected» below. - The model answers.The key is valid and, if your network needs one, the proxy works — see «the review is empty» below.
202 both when an event is accepted and when it is skipped as designed: an edit of the description or a label, an approval, a merge, a system note — none of those carry new commits. The reason is always written to the log as a ⏭ Webhook skipped line, and the response body carries queued: false. If GitLab shows a 500 in Recent events instead, look for error-level records in the log: GitLab does not retry such deliveries, and the event is lost.⏭ Skipped: head … is already reviewed line. To get a new review you need a new commit or a change of target branch. A run that did not finish does not mark the head as reviewed — a repeat after a failure goes through as usual.GitLab cannot reach the bot (the local network)
The number one cause, and an easy one to miss: a self-hosted GitLab forbids webhooks into the internal network by default. The delivery simply never happens — Recent events shows a connection error or nothing at all.
The bot does not answer, or does not start
Check the health endpoint and the logs:
curl http://localhost:3000/api/health
# {"status":"ok","ts":...,"version":"0.1.x"}docker compose logs --tail=50 app If /api/health is silent, either the container did not come up or the port is not published. When the bot sits behind a reverse proxy, pass --bot-url with its address to diagnose.sh.
«Invalid environment configuration»
The bot validates the required variables at startup and fails immediately, naming what is missing. Required: GITLAB_BASE_URL, GITLAB_TOKEN, GITLAB_WEBHOOK_SECRET (8 characters or more) and REDIS_HOST. The full reference is in Installation.
The image will not pull
The canonical image address is registry.reviewgate.dev/reviewgate-bot (our registry; the pull is anonymous). If it is unreachable, the same image sits bit for bit on the Docker Hub mirror: replace the image: line in docker-compose.yml with novohudonossor/reviewgate-bot:<the same tag>.
If Docker Hub is unreachable too — it has blocked whole regions before, and there is no guarantee it will not happen again — connect a pull-through mirror. A mirror proxies Docker Hub images only, so it works together with the previous step: image: must already carry the Docker Hub name (novohudonossor/…), which then needs no further change:
# /etc/docker/daemon.json (create it if the file is missing;
# in an existing one, ADD the key without wiping the other settings)
{
"registry-mirrors": ["https://dh-mirror.gitverse.ru"]
}
# then restart Docker (it restarts every container on the host —
# pick a quiet moment):
sudo systemctl restart dockerGitLab is unreachable, or the token is rejected
Step 3 of diagnose.sh hits GET /api/v4/user. A connection error means a wrong GITLAB_BASE_URL (it must have no /api/v4 and no trailing slash) or a network problem. A 401/403 means the token.
Token permissions
GITLAB_TOKEN is a project or personal access token with the api scope and a role of Developer or above (so that it can leave discussion comments). An expired or narrowed token gives 401/403.
The webhook was rejected: 401
A 401 on the delivery in Recent events means the bot received the webhook but the secret did not match. Compare the Secret token field of the webhook with the GITLAB_WEBHOOK_SECRET value in .env.
.env was edited but the container was not restarted, so the bot still holds the old secret. Restart it: docker compose up -d app.The webhook was rejected: 413 or 400
A 413 on the delivery means the request body is larger than the bot's limit — most often a comment event into which somebody pasted a large report or log. A 400 in the same column means the body could not be read: broken JSON or a dropped connection.
In both cases the event is lost — GitLab does not retry such deliveries, and there will be neither a review nor an answer in the thread. The refusal appears in the bot log as a separate Request body rejected line with the event type and the declared size — use it to find the delivery in Recent events and, if needed, send it again with Resend.
GitHub: the bot is silent while the webhook deliveries succeed
In the App: Advanced → Recent Deliveries shows 202, yet nothing appears on the pull request — almost always this is the set of events rather than the bot.
- The event is not in the subscription. A question under the summary arrives as an Issue comment event, and that event only appears in the App's list once the Issues (read) permission is granted. Replies in comment threads are the Pull request review comment event.
- The permissions were granted but not accepted by the installation. Ticking a box changes the permissions of the App only; every installation accepts them separately (Configure → «Accept new permissions»). Until then GitHub sends the old set of events, which looks like silence without errors.
- Replies are switched off altogether. Reply mode is off by default — it is enabled by
reply.enabledin.reviewgate/config.yml(orREPLY_ENABLEDin the bot environment). The bot log says so directly. - Under the summary the bot answers only a mention of its account: the PR conversation on GitHub is not threaded, and without that rule the bot would butt into human conversations.
A quick check of the actual state: GET /app (what the App has) against GET /repos/{owner}/{repo}/installation (what actually applies to the installation) — the difference in permissions/events is the answer.
Redis is unreachable
The queue is mandatory: the webhook only enqueues a job and the worker does all the work. A green /api/health says nothing about Redis — the answer is built without touching the queue, and the Redis client connects lazily, so the bot comes up and replies status:ok with Redis down. Check Redis itself: docker compose ps redis, and the bot log, where a connection error to REDIS_HOST/REDIS_PORT is written out.
In the log: «the job was declared stalled»
The queue considers a job stalled when the lock renewal stops reaching Redis — usually because Redis itself sagged or the worker is overloaded, not because the review is long (renewal survives that). The job is handed out again, but a second paid run almost never happens: the duplicate waits for the first to finish (up to 10 minutes) and then sees that the head has already been reviewed — a ⏭ Skipped line in the log and a repeat_head status in the metrics. A run can double up if the first one lasts longer than the delays: then the log carries a record about the delays being exhausted next to it. Isolated records can be ignored; regular ones are a reason to look at the availability of Redis and at REVIEW_CONCURRENCY.
The review is empty, or a comment about a key error
The bot publishes findings, but when the model is unavailable a notice with the reason goes into the summary (bring your own key: the bot does not guess on your behalf). Step 6 of diagnose.sh checks the key with GET /v1/models and spends no tokens: 401/403 means the key is wrong or out of credit; a connection error is covered by «a proxy to the model» below.
A proxy to the model
If your network cannot reach the provider directly, you need LLM_PROXY (a blind CONNECT tunnel; TLS to the provider stays end to end and the proxy sees only the host, never the code). If the key is set but there is no connection, check that the proxy is alive and its credentials are right. A local model (Ollama, vLLM) needs no proxy, and neither does a provider your network already reaches — more in Models and your own key. For OpenAI-compatible providers (DeepSeek, OpenRouter, Ollama, vLLM — not Anthropic, whose SDK retries connections on its own) a stream the provider breaks mid-answer (the log says stream interrupted … retrying 1/1) is retried once on its own before it counts as a network error; the retry is also shown in the run diagnostics and in usage.calls[].retries of the report, since the interrupted attempt's tokens are not counted.
The review did not run: the context did not fit the model window
The bot says in the request that the context does not fit the model window, and the logs carry prompt is too long. Usually the reason is that very large or generated code got into the changes: it is many times bulkier than handwritten code and there is no point reviewing it. Most often this surfaces with llm.full_file_context enabled, when the bot takes the changed files whole.
What helps, in order:
- add the generated files to
ignorein config.yml —**/*.g.dart,**/*.freezed.dart(Flutter),**/*.generated.ts, snapshots, bundles, vendored directories; - switch
llm.full_file_contextoff, so the bot looks only at the diff; - split the request into smaller pieces;
- on a model with a small window (usually a local one), lower
LLM_DIFF_CHAR_BUDGETandLLM_MAX_TOKENS: the requested answer size counts towards the window too.
There are no retries on this error: the result would not change — the config has to be edited. If the 🔬 diagnostics block is enabled, the «Model context» line of the next successful review shows how many files did not fit the budget.
The review did not run: the answer did not fit the output limit
The mirror image: everything fitted the model window, but the answer hit the LLM_MAX_TOKENS ceiling (the logs show truncated by max_tokens or finish_reason=length). This usually happens on very large requests: on reasoning models the internal reasoning spends the same limit as the answer itself, and the volume of findings grows with the diff.
The bot first tries to cure it itself: on Anthropic models it repeats the request with reduced reasoning (on OpenAI-compatible ones there is no repeat — at temperature: 0 it would be truncated in exactly the same way). If that does not help it writes a notice into the request and spends no money on blind retries: every repeat would pay for the whole input again. What helps:
- raise
LLM_MAX_TOKENSin the bot environment — within the output ceiling of your model, which differs a lot between providers (from a few to tens of thousands of tokens); check the model documentation. Set it above the ceiling and the provider starts rejecting the requests, which the bot reports with a separate notice; - split the request into smaller pieces — large diffs are reviewed worse anyway;
- exclude generated files with
ignorein config.yml — fewer findings, a shorter answer.
The review did not run: the provider is overloaded or rate limiting
The logs show overloaded_error (HTTP 529 at Anthropic), a 503/529 with «overloaded» from an OpenAI-compatible provider, or a prolonged 429 (a rate limit — the request or token allowance of your tier is used up). This is a state on the provider's side: the key, the network and the configuration usually have nothing to do with it.
The bot cures this itself: it repeats the review with growing pauses (several attempts over a window of about 7 minutes), and the overload usually passes within that time, after which the review is published as usual. If it does not pass, the bot writes a notice into the request and starts again on the next push.
When it happens often on a stream of requests, check the provider's status page and your tier limits; it helps to pick a less loaded model from the same vendor (the first role in llm.generators in config.yml) or to change the vendor of the main review — the backend of the role, or LLM_PROVIDER/LLM_MODEL in the bot environment. The extra roles (further generators, llm.judges, llm.arbiter) will not save you here: they add calls on top of the main generator rather than replacing it — and so do the named LLM_BACKEND_<NAME>_* backends, which only serve those extra roles.
The review did not run: the endpoint or the model was not found
The request carries a notice that the provider answered but there is nothing at the address given, and the logs show 404 or model_not_found. The network is fine here: the request arrived, there is simply no answer at that address. Two usual causes are LLM_BASE_URL without /v1 on an OpenAI-compatible server (the address must point at the API, not at the root of the site), and a typo in LLM_MODEL or a model name the vendor does not have.
There are deliberately no retries: with the same configuration the answer will not change, and five attempts would only burn time. Fix the address or the model name — the next push starts a review. You can check the configuration before committing with reviewgate doctor: it probes the endpoint live and spends no tokens.
The review was stopped by the budget (🛑 in the summary)
A spending cap is set (LLM_BUDGET_TOKENS / LLM_BACKEND_<NAME>_BUDGET_TOKENS) and the spend of some backend broke through three times the limit — the run was stopped at the nearest checkpoint and what had been found by then was published (the findings may not be confirmed by a judge, which the summary says). The thresholds are fixed and not configurable: 80% a notice, 100% a loud notice (the review still completes), 300% a stop. Such an overrun is almost never «an expensive review» but an anomaly: a gigantic generated diff, a looping agent, a carousel of retries. What to do: look at the 🔬 diagnostics block (the «Run budget» line), check the contents of the diff and the role layout; if the spend is expected, raise the limit in the deployment environment. For automation the snapshot is machine-readable: the budget field in the CLI JSON report and in the MCP answer (the threshold, the spend and the limit per backend), and budget_barrier with tokens_by_backend in the deployment metrics for any surface, CLI runs included.
The key or token was copied from a messenger
The symptom varies — the bot does not start, reviewgate doctor complains about the review server, a run fails — but the cause is one: the value contains a non-ASCII character. Cyrillic с, е, р and «smart» quotes are visually indistinguishable from latin ones, and they do not pass into an HTTP header.
We name this plainly: the message gives the position of the character, the character itself and its code. The cure is to issue the key or token again and copy it whole, without selecting it with the mouse inside a chat. The check sits before the first paid call, so the defect is caught by the bot starting up and by reviewgate doctor, not in the middle of a review.
A related case is a token split across two lines while copying. A request with such a value goes out normally and gets an ordinary «no access» from GitLab, which sends people to an administrator for permissions when the real problem is a stray space. We name that in words too. Stray spaces and line breaks at the edges of a value are stripped silently — that is copying noise, not part of the token.
Logs
To read the bot logs use docker compose logs app; every line of a review carries the job tag. Centralised collection (Graylog / ELK / Loki), the structured LOG_FORMAT=json and the GELF driver recipe live on a separate page, Logs and monitoring.
Summary: symptom → cause → what to do
| symptom | cause | fix |
|---|---|---|
| The bot does not react to a request, Recent events is empty | the webhook is not configured, or is not delivered | walk the checks above; diagnose.sh --project N |
| GitHub: deliveries return 202 but the bot is silent | the event is not subscribed, or the App permissions were not accepted by the installation | the «GitHub: the bot is silent…» section above |
| Recent events: a connection error or a timeout | GitLab cannot reach the internal network | enable Outbound requests (the «local network» section) |
| In the request: «the endpoint or the model was not found» | LLM_BASE_URL without /v1, or a wrong model name | fix the address or the model; check with reviewgate doctor |
| Recent events: 401 | the webhook secret did not match | compare the Secret token with .env and restart the bot |
| Recent events: 413 or 400 | the event body was not read — it is lost | upgrade the image; resend the delivery with Resend |
docker compose pull fails or hangs | the registry is unreachable | a Docker Hub mirror (the «the image will not pull» section) |
| The bot does not start, the logs say the configuration is invalid | a required variable is missing | fill in .env (the configuration section) |
/api/health does not answer | the container did not come up, or the port is not published | read the logs; pass --bot-url |
| The review is empty, or a comment about a key error | the model key is invalid or out of credit | check the model key and the balance |
| The logs show a timeout to the provider | no working proxy | set LLM_PROXY |
«The context does not fit», the logs show prompt is too long | large or generated code got into the changes | ignore for the generated files; see the section above |
«The answer did not fit the output limit», the logs show truncated by max_tokens | the model reasoning plus the volume of findings exceeds LLM_MAX_TOKENS | raise LLM_MAX_TOKENS; see the section above |
«The provider is overloaded», the logs show overloaded_error | a temporary load on the provider side | it usually passes on its own; see the section above |
| The summary says Community with «the key belongs to another deployment» | the license was issued for a different GitLab | check the license |