Daily runs, weekly batches: splitting Renovate's trigger from its policy on self-hosted GitLab
A single daily GitLab schedule keeps Renovate's vulnerability detection fast while a repository-level schedule window collapses routine dependency churn into one weekly review batch.
TL;DR: I keep the GitLab CI pipeline schedule daily (0 2 * * * UTC) so Renovate detects security advisories within 24 hours (vulnerabilityAlerts ignores schedules). In renovate.json5, I set schedule: ["* 0-11 * * 1"] and updateNotScheduled: false to collapse routine updates into a single Monday morning batch, raise prConcurrentLimit to 20 to handle all 19 branch groups simultaneously, and use RENOVATE_FORCE for on-demand manual runs.
This is the fourth post in a series: config hardening, closing the transitive-CVE blind spot, and self-hosting Renovate as a scheduled GitLab CI job set up the bot; clearing thirty Renovate merge requests in one pass cleaned up the backlog. This post removes the source of that backlog.
Note
Observed on self-hosted GitLab CI with Renovate in September 2026.
The Problem
The setup from the previous posts: one GitLab pipeline schedule runs the renovate:run job daily at 02:00 UTC, and the repository config had no schedule at all. Every daily run evaluated every branch, so Renovate force-pushed all routine branches on every pass.
The measured behavior over three weeks:
- 88 routine merges, all in weekly bursts of 18–24; the maintainer already reviewed weekly.
- One persistent branch accumulated 23 pipelines in a single day from repeated force-pushes.
- Burst days reached 91 pipelines; jobs waited up to 20 minutes just to start.
- Between bursts, the daily churn bought nothing: branches regenerated the day after each merge, waiting for the next review session.
Daily runs remain necessary for security: Renovate must execute to evaluate new advisories. Setting schedule: null under vulnerabilityAlerts lifts internal scheduling limits, but it cannot trigger an idle bot. Dropping to a weekly GitLab pipeline schedule would delay zero-day detection by up to six days.
Daily vulnerability detection requires frequent execution, while maintainer review favors batched updates.
What Changed
Renovate’s scheduling model separates triggering from branch creation. As the documentation notes, the schedule option “restricts when Renovate is permitted to create branches. It does not trigger Renovate runs.” Triggering remains in GitLab CI pipeline schedules, while branch policy lives in repository configuration:
// renovate.json5
timezone: "UTC",
schedule: ["* 0-11 * * 1"], // Monday 00:00–11:59 UTC
updateNotScheduled: false,
Key configuration decisions:
- Root-level scheduling gates all routine updates. Setting
scheduleat the configuration root covers patches, minors, toolchains, and digests. Configuringscheduleinside per-packagepackageRulesbreaks grouped branches: underseparateMinorPatch: false, patches and minors share a single branch per lockfile, and package-level schedules delay the entire group. updateNotScheduled: falsehalts mid-week rebase churn. The default (true) continues updating existing branches outside the schedule window. Settingfalsekeeps open branches idle until the next window opens.- Security and dashboard approvals bypass the window. Vulnerability alerts bypass schedules by default (their forced preset sets
schedule: null), opening PRs on the next daily run. Dashboard-approved majors receiveschedule: ["at any time"]via a package rule; because major updates isolate to separate branches, they do not stall grouped patch/minor branches.
This matches Renovate’s noise-reduction guide: schedule routine batches weekly while keeping security updates immediate.
Concurrency must cover the batch. Restricting routine updates to a weekly window opens all eligible merge requests simultaneously on Monday. The repository defines 19 concurrent branch groups (7 manager groups, 6 pnpm workspaces, 5 toolchain groups, 1 digest group). Raising prConcurrentLimit from 10 to 20 prevents rate-limited branches from waiting an entire week for the next window.
The window must accommodate pipeline retries. An initial 4-hour window (* 0-3 * * 1) blocked manual retries after transient failures at 02:00: because Renovate evaluates the schedule window during manual replays, an out-of-window replay at 09:00 skips all branches, deferring the batch by a week. Expanding the window to 12 hours (* 0-11 * * 1) keeps mid-week replays quiet while giving Monday recovery attempts the full morning.
Bypassing the window entirely requires RENOVATE_FORCE='{"schedule":["at any time"]}' (precedence notes). RENOVATE_SCHEDULE fails here because Renovate merges repository configuration over environment variables; only the bot-admin force object is merged last and overrides repository settings.
The daily GitLab pipeline schedule remains in place: a health-check job asserts the schedule exists, triggers at 0 2 * * * UTC, and records a successful run at least once every 36 hours.
Results
The runner, not the schedule, was the other bottleneck. Auditing 2,000+ jobs showed the recurring 504 Gateway Timeout lines in the runner log had never failed a single job; they are transient long-poll noise between the runner and gitlab.com. Queued jobs on self-hosted runners do not time out, leaving capacity as the only lever. The runner runs 4 concurrent slots, which covers the Monday batch at the measured job profile (median 1.6 min, mean 2.2): roughly 300–450 runnable jobs, about 3–4 hours of saturated runner. That is overnight-friendly, and the same total work as before, just contiguous.
The observable steady state:
- Tuesday through Sunday: daily runs process security alerts and the dependency dashboard only. Pending routine updates stay visible daily; their branches do not regenerate.
- Monday 02:00 UTC: all routine groups open or update at once, one review batch, with the patch/digest auto-merge lanes merging as their pipelines go green.
The first weekly batch is the real test; the numbers above are projections from the measured baseline, not observations yet.
What I’d Do Differently
Initial testing caught two sizing errors. The narrow 04:00-closing window ignored that manual replays are filtered too; a scheduler-recovery concern I only caught when stress-testing how the mechanism activates. And I initially read the runner’s concurrency from the tracked config template (2 slots) instead of from evidence; job traces showed build containers named ...-concurrent-3, impossible at 2 slots. The live host had drifted ahead of its template. Config templates record intent; verify the running state before doing capacity math on them.
References
- Renovate scheduling. The global-versus-specific scheduling model.
- Renovate
scheduleoption. “does not trigger Renovate runs”. - Renovate noise reduction. Official guidance to schedule routine batches weekly.
- Renovate vulnerability alerts. Alerts bypass scheduling constraints.
- GitLab pipeline schedules. The trigger layer.
- Self-hosting Renovate on GitLab CI. The execution model this builds on.
- Clearing thirty Renovate merge requests in one pass. The backlog this removes at the source.
- RENOVATE_SCHEDULE loses to repo config; RENOVATE_FORCE wins. Source-trace of the precedence override.
This post was written with AI assistance.