Renovate EE - Managing Monorepos with Renovate Enterprise

Audience: Teams running Mend Renovate Enterprise Edition who manage one or more large monorepos.

Overview: This article provides guidance for configuring Renovate Enterprise so that large monorepos are scanned efficiently, produce clean and reviewable pull requests, and do not consume resources at the expense of the rest of your fleet. It covers the repository-level configuration under your control, the Enterprise scaling features designed for large repositories, and the guardrails and best practices recommended for production deployments.

Who configures what. Renovate Enterprise is configured on two surfaces. Repository configuration lives in each repository's renovate.json or renovate.json5 and is edited by developers. It covers the repository-scoped options in this article, such as path scoping, grouping, and update behavior. Administrator configuration lives in the bot config.js, in environment variables such as the MEND_RNV_* settings, and in options that Renovate accepts only at the global level, and it is managed by your Renovate administrators. Where a setting in this article is administrator-only it is labeled as such.

1. The Monorepo Cost Model

A monorepo concentrates many package files, workspaces, and often several ecosystems into a single repository. Renovate's cost for any repository scales with three multipliers working together:

(number of enabled managers) x (package files each manager finds) x (dependencies per file)

The single most expensive operation is lockfile and artifact regeneration. After Renovate determines an update, it runs the project toolchain (npm install, yarn, pnpm install, go mod tidy, Gradle resolution, and others) to regenerate the lockfile. This is a full dependency resolution that can take several minutes, and it runs once per update branch (that is, per open pull request carrying a dependency change), not once per repository scan. This phase is distinct from the earlier extract and dependency-lookup phase, which runs once per base branch (the default branch plus any branches resolved from baseBranchPatterns). In a monorepo with many concurrent update branches, the per-update-branch regeneration is the dominant cost.

The practical implications for a large monorepo:

  • Extraction is broad. Every enabled manager pattern-matches recursively across the entire checkout, then parses every match.

  • Lookups accumulate across runs. Each distinct dependency is looked up once per run (occurrences across workspaces are de-duplicated within a run). On workers with no persistent or shared cache, that version metadata is re-fetched on every scheduled run, which the Caching Strategy section addresses.

  • Lockfile work dominates run time. Regenerating lockfiles is the largest factor in how long a run actually takes from start to finish (its wall-clock time, the real elapsed duration a user waits, as opposed to CPU time). More open update branches means more lockfiles kept alive and rebased when they fall behind the base branch.

  • Clone size and memory grow with the tree.

Renovate Enterprise provides both the repository-level controls to reduce the cost of each run and the horizontally scalable server architecture to process many jobs in parallel. The remainder of this article describes how to combine them.

Important Limitation When Integrating with GitHub

When Renovate Enterprise integrates with GitHub, it authenticates as a GitHub App, and the installation token that GitHub issues expires after 60 minutes. This is a GitHub platform constraint that cannot be extended, and Renovate does not refresh the token during a run. A scan still in progress when the token expires fails mid-run, and the failure is easy to misdiagnose. The job may end with a bad-credentials or authentication-error result, or the log may simply stop, with no error and no Repository finished marker. Pull requests created before that point are unaffected, but the remaining updates are not processed, and the next scheduled run repeats the same failure until the scan duration is reduced. In practice, failures of this kind have been observed shortly before the 60-minute mark, so treat roughly 50 minutes of scan time as the practical ceiling on GitHub. The scoping, caching, and lockfile controls in this article are the tools that bring a large monorepo under that boundary, and raising the worker execution timeout above 60 minutes does not extend it. Check the Mend Renovate release notes for changes to this behavior. GitLab and Bitbucket connections use long-lived personal access tokens and are not affected.

2. Onboarding a Large Monorepo Safely

The initial scan of a monorepo carries the most risk. With default settings, the first scan of a dependency-heavy repository can queue hundreds of updates at once, overwhelm reviewers with pull requests, and place a sudden load on your registries. Renovate provides ways to see the full workload before any of it is created.

  • Dry runs (administrator-only). The bot-level dryRun setting (extract, lookup, or full) runs the scan and logs what would happen without writing anything to the repository. Run full on the monorepo once to measure scan time and count the pending updates before enabling real runs.

  • Silent mode. Setting "mode": "silent" makes Renovate scan without creating pull requests, issues, or comments. It is a useful trial state for a repository that is onboarded but not yet ready to receive update volume.

  • Onboarding defaults (administrator-only). onboardingConfig controls the configuration proposed in the onboarding pull request. Seed it with conservative defaults (low concurrency limits, config:recommended, the Dependency Dashboard) so a large repository begins with restrictive limits already in place.

Once real runs begin, dependencyDashboardApproval (covered later in this article) lets you work through the initial backlog in deliberate batches.

3. Scoping the Scan

The most efficient run is one that avoids parsing files it does not need to process. These controls reside in your repository configuration (renovate.json, renovate.json5, or the equivalent) and represent the most impactful changes available.

includePaths and ignorePaths

includePaths is an allowlist. In a large repository it is the most effective single control. Renovate only discovers package files under the paths you specify.

JSON
{
  "includePaths": ["apps/**", "packages/**", "services/**"]
}

ignorePaths excludes subtrees. By default Renovate ignores node_modules and bower_components, and the :ignoreModulesAndTests preset (included in config:recommended) extends that to common test, fixture, vendor, and example directories.

Note: ignorePaths is not a mergeable option. A value set in your repository config replaces the preset defaults entirely rather than adding to them, so an override that lists only your extra paths silently re-enables scanning of node_modules, test, and fixture trees. When you need additional exclusions, restate the full list:

JSON
{
  "ignorePaths": [
    "**/node_modules/**",
    "**/bower_components/**",
    "**/vendor/**",
    "**/examples/**",
    "**/__tests__/**",
    "**/test/**",
    "**/tests/**",
    "**/__fixtures__/**",
    "**/testdata/**",
    "**/generated/**"
  ]
}

enabledManagers

Every enabled manager performs a full recursive match and parse. If your monorepo uses only JavaScript and Go, disabling the remaining managers eliminates a significant amount of unnecessary work:

JSON
{
  "enabledManagers": ["npm", "gomod"]
}

Base Branches Multiply the Cost

If you use baseBranchPatterns to run Renovate against release branches in addition to the default branch, every matched base branch adds a full extraction and lookup pass and carries its own set of update branches, so the cost model from section 1 applies once per base branch. On a monorepo, keep the list to the minimum set of active release streams, and prefer exact branch names over regex patterns, which can silently match many branches.

Per-directory configuration with matchFileNames

Use a single root configuration that scopes rules by path rather than distributing configuration across many nested files. matchFileNames in packageRules targets dependencies by the location of their package file:

JSON
{
  "packageRules": [
    {
      "matchFileNames": ["apps/web/**"],
      "additionalBranchPrefix": "web-",
      "reviewers": ["team:web"]
    },
    {
      "matchFileNames": ["services/api/**"],
      "additionalBranchPrefix": "api-",
      "reviewers": ["team:api"]
    }
  ]
}

additionalBranchPrefix separates branches by area (for example renovate/web-... versus renovate/api-...), which clarifies ownership and allows teams to filter their own pull requests.

Note: matchPaths has been removed. It is auto-migrated to matchFileNames, which you should use for all path-based rules.

Automatic Workspace Handling

npm, Yarn, and pnpm workspaces are fully supported with no configuration. Renovate resolves the workspace structure, recognizes packages that are internal to the repository, and excludes them from updates, so it never opens a pull request to update one of your own packages. Lerna- and Nx-based monorepos are handled through this same npm workspaces support rather than a dedicated manager. The same applies beyond JavaScript. Go modules (gomod) and Gradle multi-project builds, including shared version catalogs, are discovered by their own managers with no special setup.

If your workspaces use a pnpm or Yarn catalog to declare shared dependency versions in one place, Renovate updates the catalog entry once instead of raising a separate update for each workspace that uses it, which reduces both pull request volume and lockfile churn.

4. Controlling Lockfile Regeneration

Because lockfile regeneration dominates run time, controlling it yields the largest performance improvements.

Scheduled Lockfile Maintenance

By default, Renovate does not recreate the entire lockfile on a routine run. On each update branch it performs a targeted lockfile update for that branch's specific dependency change (for example npm install <package>), leaving all other locked versions untouched. This per-update-branch update is the baseline recurring cost described earlier, and it is unavoidable, because each update branch needs its own consistent lockfile.

lockFileMaintenance is a separate, opt-in feature (disabled by default). When enabled, it opens a dedicated "Lock file maintenance" pull request that deletes and fully recreates the lockfile, picking up transitive and indirect dependency updates that the targeted per-update changes do not. It is additive rather than a replacement: it does not reduce the per-update lockfile work, and its schedule controls only when the full recreate runs.

Support for lockFileMaintenance varies by package manager. It works for npm, pnpm, Yarn, and Poetry among others, but the Maven manager does not support it, and Gradle requires the administrator to allow the Gradle wrapper through allowedUnsafeExecutions (toolSettings.jvmMaxMemory bounds the JVM heap for those wrapper updates, default 512 MB).

Because a full recreate is expensive, keep it on an off-peak schedule rather than allowing it to run during working hours. A weekly or monthly cadence is usually sufficient:

{
  "lockFileMaintenance": {
    "enabled": true,
    "schedule": ["* 0-4 * * 1"] // before 5am on Monday
  }
}

This schedule is a repo-level setting. It applies only when the server scheduler runs the repository inside the window, and it has no effect if your administrators have forced schedules globally. See Run Scheduling for how the two layers interact.

Note that lockFileMaintenance pull requests are still subject to your concurrency limits. If prConcurrentLimit or branchConcurrentLimit is already reached, the maintenance pull request is not created (Renovate reports branch-limit-reached or pr-limit-reached). The branch and pull request limits are checked separately, so a full exemption must lift both. This uses the same per-rule limit mechanism described under Per-Manager Limits in the Pull Request Quality section. Alternatively, trigger the maintenance run manually from the Dependency Dashboard:

JSON
{
  "packageRules": [
    {
      "matchUpdateTypes": ["lockFileMaintenance"],
      "branchConcurrentLimit": 0,
      "prConcurrentLimit": 0
    }
  ]
}

Range Strategy Selection

Renovate's default rangeStrategy is auto, and for a monorepo it is usually the right choice. auto resolves to a manager-specific behavior. For npm it updates the lockfile in place when a new version is in range, widens peerDependencies and complex ranges when needed, and edits the manifest when an update is out of range. Both config:recommended and config:best-practices rely on auto and set no global rangeStrategy.

For a set of applications you may still want lock-only churn, where Renovate keeps the declared range fixed and moves only the lockfile for in-range updates. This is attractive in a monorepo for a few reasons. When many applications share the same declared ranges, keeping those ranges stable and updating only the lockfile produces small, uniform diffs that are easier to review and safer to automerge. It reduces merge conflicts between concurrent update branches, since the manifest version strings do not change. For deployable apps that value reproducible builds, it keeps the resolved versions current within the compatibility ranges the team has deliberately chosen, rather than rewriting those ranges on every update.

Apply this with a scoped packageRule on your application paths rather than globally, so that library packages stay on auto and keep their ranges free to widen. When an update falls outside the declared range, update-lockfile still edits the manifest, so dependencies are not held back. Pair it with scheduled lockFileMaintenance so transitive updates are also picked up:

JSON
{
  "packageRules": [
    {
      "matchFileNames": ["apps/**"],
      "rangeStrategy": "update-lockfile"
    }
  ]
}

Post-Update Options

Each value in postUpdateOptions adds work, and in a monorepo that cost multiplies per workspace. npmInstallTwice performs the install step twice, and the Go options (gomodTidy, gomodVendor, gomodUpdateImportPaths) add extra passes. Enable only what a given path requires, scoped with packageRules where possible.

Toolchain Constraints

Declaring your toolchain versions stops Renovate from probing or installing multiple tool versions and ensures lockfile regeneration matches your environment, which avoids spurious diffs and retries:

JSON
{
  "constraints": {
    "node": "20",
    "pnpm": "9"
  }
}

How the constrained version is provided depends on the worker image and its binarySource mode. The Enterprise slim image runs with binarySource=install, where Renovate downloads and installs the required tool version at runtime, so constraints directly selects what gets installed and nothing needs to be preinstalled. The full image runs its preinstalled tools with binarySource=global, and in that mode constraints does not select tool versions at all. The versions baked into the image are what run. If pinning toolchain versions through constraints matters for your monorepo, use the slim image with install mode.

Install Memory for Large JS Workspaces

Lockfile regeneration in a large JavaScript monorepo runs npm, yarn, or pnpm install, and these installs are the most common source of out-of-memory failures. toolSettings.nodeMaxMemory sets the Node heap (--max-old-space-size) for the install process and can be set by developers in renovate.json, subject to a global cap. For pnpm workspaces, the PNPM_WORKERS and PNPM_MAX_WORKERS variables reduce pnpm's parallel-worker memory during install. These are pnpm's own environment variables rather than Renovate options, so a Renovate administrator sets them on the worker (they are already on Renovate's environment allowlist) or through customEnvVariables in the bot configuration.

This is distinct from the worker-level MEND_RNV_NODE_OPTIONS, which sizes the Renovate process wrapping the whole scan rather than the package-manager install.

5. Caching Strategy

Caching substantially reduces the cost of repeat runs on a large monorepo. In Enterprise, workers honor the underlying Renovate cache settings, and a shared cache allows scaled workers to avoid duplicating each other's work. Every setting in this section is administrator-configured in the bot config.js or through environment variables, not in repository config.

Repository Cache

repositoryCache persists Renovate's per-repository state between runs. This includes the extracted package files for each base branch (so unchanged files are not re-scanned on the next run), along with branch, pull request, and onboarding state. This is the single most impactful caching setting for a monorepo that is scanned repeatedly. It is a self-hosted (bot-level) setting, not a repository option, so set it in your Enterprise bot configuration rather than in a repository's renovate.json:

JSON
{
  "repositoryCache": "enabled"
}

The cache can be stored locally or in S3 via repositoryCacheType (also a bot-level setting), which is useful when workers are ephemeral.

Renovate Enterprise does not enable this cache by default. Set it on the workers with the RENOVATE_REPOSITORY_CACHE=enabled environment variable. An enabled repository cache is also a prerequisite for the Enterprise repository pull-requests API.

Shared Datasource Cache with Redis

A large monorepo has a high number of distinct dependencies, and every scheduled run looks up version and changelog metadata for each one. A shared Redis datasource cache (redisUrl / redisPrefix) turns that recurring, network-bound cost into cache hits. Point all Renovate Enterprise workers at it so a lookup by one worker serves the rest instead of each re-fetching the same data. Its value grows with the size of the dependency graph, which makes it essential for a monorepo once you run multiple workers. cacheHardTtlMinutes complements this by keeping stale-but-usable entries, so when an upstream request fails (for example a rate-limiting 429 during a run that issues thousands of lookups) Renovate can still serve the cached data. The option is experimental and has no useful effect until set to a non-zero value, with 60 as the documented minimum to start from.

By default Renovate caches only public-package results, and only for datasources that opt into caching (the major ones, including npm, PyPI, and Maven, do). Setting cachePrivatePackages: true (a self-hosted setting) also persists private results, so later runs serve that metadata from the cache instead of re-querying the registry.

This matters most for proxy and virtual registry topologies. When all dependencies resolve through a single authenticated proxy (such as Artifactory or Nexus) that also mirrors open source, Renovate classifies nearly everything as private. Maven results count as private because the registry is not Maven Central, PyPI results because the requests are authenticated, and npm results because the proxy does not send Cache-Control: public. Public-package caching then never applies, and the monorepo's entire dependency surface is re-fetched every run, so enabling cachePrivatePackages with a shared Redis cache is effectively required to get any cross-run caching. Keep that cache in your own infrastructure, since it holds your private package metadata.

Git Data Between Runs

Renovate already performs a blobless clone by default (--filter=blob:none), which keeps the initial clone of a large monorepo far smaller than a full clone. On stateful workers you can go further with the bot-level persistRepoData: true, which keeps the checkout between runs so the next scan performs a fast git fetch instead of a fresh clone, and preserves ignored directories such as node_modules, cutting install time.

Note: persistRepoData conflicts with aggressive worker cleanup. The MEND_RNV_WORKER_CLEANUP job described later clears exactly these directories, so on a worker pool that persists repository data, schedule cleanup sparingly and deliberately.

6. Pull Request Quality and Volume Control

Efficiency is only part of the objective. A monorepo that produces an excessive volume of pull requests is as difficult to manage as one that scans slowly. The following settings keep output reviewable and distribute load.

Concurrency and Rate Limits

Fewer live branches means fewer lockfiles kept alive and rebased when they fall behind the base branch, which reduces both noise and recurring cost:

JSON
{
  "prConcurrentLimit": 10,
  "branchConcurrentLimit": 20,
  "prHourlyLimit": 2
}
  • prConcurrentLimit caps simultaneously open pull requests. Note that 0 means unlimited rather than zero, so use a positive value to impose a cap (the same applies to branchConcurrentLimit and prHourlyLimit).

  • branchConcurrentLimit caps open branches (defaults to prConcurrentLimit if unset).

  • prHourlyLimit rate-limits pull request creation, which is especially valuable when first onboarding a dependency-heavy monorepo. The hour is the current UTC clock hour, not a rolling 60-minute window.

Per-Manager Limits

These three limits can also be set inside packageRules, so different package managers in the same monorepo can have different limits. A limit set in a package rule replaces the top-level value for the updates that rule matches. The top-level value continues to apply to every update no rule matches. This lets you prioritize the ecosystems that matter most while holding back noisier ones:

JSON
{
  "prConcurrentLimit": 10,
  "packageRules": [
    {
      "matchManagers": ["npm"],
      "prConcurrentLimit": 5
    },
    {
      "matchManagers": ["gomod"],
      "prConcurrentLimit": 3
    },
    {
      "matchManagers": ["dockerfile", "docker-compose"],
      "prConcurrentLimit": 0
    }
  ]
}

In this example, npm updates are limited at 5 instead of the top-level 10, gomod updates at 3, Dockerfile and Docker Compose updates are unlimited (0 means no limit), and updates from any other manager fall back to the top-level 10. These are not independent quotas per manager. Renovate keeps one repository-wide count of open pull requests, and the per-rule value changes only the threshold each branch is compared against. In the example, gomod branches stop being created once the repository has 3 open Renovate pull requests in total, from any manager, and npm branches stop once the total reaches 5. The practical effect is prioritization: lower-priority ecosystems get an earlier cutoff while higher-priority ones keep flowing.

Note on grouped updates: a branch that combines updates matched by different rules, which happens when a groupName spans managers, takes the lowest limit among them. A group mixing npm and gomod updates would stop at 3 in the example above. The exception is a group that includes even one update whose limit resolves to 0 or is unset. That branch becomes exempt from the limit entirely. This behavior is available in recent versions of the Renovate CLI.

Rebasing Cost

Rebasing is the other factor in the recurring cost of open branches. When Renovate rebases an open branch, it re-runs the full lockfile and artifact regeneration for that branch, so the recurring cost is roughly the number of open branches multiplied by how often each is rebased. The default rebaseWhen: "auto" is the cost-aware choice. It rebases a behind-base branch only when it has to (automerge is enabled, a keep-updated label is set, or branch protection requires branches to be up to date), and otherwise rebases only on a genuine merge conflict.

Avoid setting rebaseWhen: "behind-base-branch" fleet-wide on a busy monorepo, since it rebases every open branch each time the base branch advances. rebaseWhen: "conflicted" avoids that but lets branches drift behind the base, so reserve it for cases where you are not relying on automerge. A newer automerging value keeps automerging branches up to date and otherwise never rebases, which pairs naturally with the automerge strategy below.

Automerge for Low-Risk Updates

The concurrency limits above cap how many branches exist at once. Automerge shortens how long each one lives, which addresses the same cost from the other direction. A branch that passes its checks and merges promptly is never rebased.

Scope automerge to update classes your team routinely merges without discussion, such as patch and minor updates of devDependencies or digest pins, and reserve major updates of production dependencies for human review:

JSON
{
  "packageRules": [
    {
      "matchDepTypes": ["devDependencies"],
      "matchUpdateTypes": ["minor", "patch"],
      "automerge": true
    }
  ]
}

Two options make automerge cheaper at monorepo scale. automergeType: "branch" merges the update branch directly once tests pass and never opens a pull request at all (one is opened only if tests fail), which removes pull request volume entirely for the low-risk classes. platformAutomerge delegates the merge to the platform so it happens the moment checks pass, instead of waiting for Renovate's next run. Renovate also supports GitHub's Merge Queue where your organization has it enabled.

Dampening Release Churn

Fast-releasing packages cause repeated branch updates, each with its own lockfile regeneration. Requiring a release to age before Renovate acts on it cuts that churn directly, and doubles as protection against releases that are quickly unpublished or turn out to be compromised:

JSON
{
  "minimumReleaseAge": "3 days",
  "prCreation": "not-pending",
  "internalChecksFilter": "strict"
}

minimumReleaseAge holds an update until the release has aged, prCreation: "not-pending" delays pull request creation until that check passes instead of opening a pending one, and internalChecksFilter: "strict" selects the newest version that passes the age check rather than skipping the update.

Changelog Fetching

Renovate fetches release notes for every dependency in every pull request body. On a monorepo producing a high pull request volume this is a substantial network cost, and it counts against your platform's API rate limits. If changelogs add little review value for a class of updates, turn them off globally or per path with "fetchChangeLogs": "off", then re-enable them by package rule where the notes genuinely inform review. Avoid "fetchChangeLogs": "branch", which the documentation warns slows Renovate down.

Dependency Dashboard

Enable the Dependency Dashboard so a single tracking issue lists every pending, rate-limited, and blocked update. This allows you to keep concurrency limits low without losing visibility. Nothing is hidden. Each update is simply queued and listed. It is the recommended approach for managing a large backlog without overwhelming reviewers.

For the first run on a large monorepo, you can go further and gate updates behind manual approval. Setting dependencyDashboardApproval: true holds every update in the dashboard until you tick its checkbox, so no pull requests are created until you approve them. You then work through the backlog in deliberate batches instead of receiving a large volume of pull requests at once.

JSON
{
  "dependencyDashboardApproval": true
}

This differs from the rate limits above, which still create pull requests automatically and only slow the pace. The two combine well. The approval gate is applied first, and the rate limits then throttle whatever you approve. Applied inside packageRules, approval can be required only for higher-risk updates such as majors while routine updates flow automatically.

Grouping and Separation

Group updates that are genuinely released together and keep unrelated updates separate. Over-grouping carries real risk. One broken package in a large group blocks the entire pull request, and a single major update can hold back unrelated updates grouped with it. Use groupName (and groupSlug to control the branch name) for tightly coupled sets:

JSON
{
  "packageRules": [
    {
      "matchPackageNames": ["/^@myorg/ui-/"],
      "groupName": "myorg ui components",
      "groupSlug": "myorg-ui"
    }
  ]
}

Registry Protection with hostRules

For registries that are sensitive to load, throttle Renovate so it does not trigger rate limiting (HTTP 429s):

JSON
{
  "hostRules": [
    {
      "matchHost": "registry.internal.example.com",
      "concurrentRequestLimit": 5,
      "maxRequestsPerSecond": 10
    }
  ]
}

concurrentRequestLimit caps how many requests Renovate has in flight to that host at once, and maxRequestsPerSecond caps how fast it issues them. In the example, Renovate keeps at most 5 requests open to the registry and sends no more than 10 per second. This is useful for a monorepo, where the large number of dependency lookups can otherwise overwhelm an internal registry.

Run Scheduling

For a large monorepo, when Renovate runs matters as much as how often. A monorepo scan is expensive, and its heaviest work, especially the full lockfile recreate, is best confined to off-peak hours rather than competing for resources during the day. Getting that right depends on understanding that scheduling happens at two independent layers. Mend documents the full model in Renovate EE job processing.

  • The server-side scheduler (the HOT, COLD, CAPPED, and ALL cron tiers covered in the next section) decides whether and when a repository's job runs at all. It lives in the global bot configuration and is managed by your Renovate administrators.

  • A repo or packageRule schedule is a filter applied during a run. It decides whether Renovate may open or update branches at the current moment.

An action occurs only when both layers agree. As the Mend documentation states, a defined schedule is honored only if the repository is processed within the time window the schedule defines. If a job fires outside that window, Renovate creates nothing and lists the update as awaiting schedule in the Dependency Dashboard.

Monorepos are the most exposed to this mismatch. A heavy monorepo is the kind of repository most likely to sit on a lower-frequency tier, because resource limits and timeouts demote it to CAPPED or ALL, so a developer's narrow schedule window is especially unlikely to align with an actual run and the action can silently never happen. Prefer wide windows, and treat the server scheduler as the layer that actually governs timing.

If schedules are forced globally. Some Renovate admin teams standardize timing by overriding all schedules to always-on with a force on schedule in the bot config.js, so that developer-defined windows cannot create false expectations. Where this is in place, repo-level and packageRule schedules have no effect, a forced top-level schedule also overrides a repo's lockFileMaintenance schedule, and timing is governed entirely by the server scheduler. Developers should raise off-hours or cadence requirements with the admin team rather than setting schedule in repo config. Administrators who force schedules globally and still want a monorepo's full lockfile recreate to run off-peak can force that schedule specifically:

{
  "force": {
    "lockFileMaintenance": {
      "schedule": ["* 0-4 * * 1"] // before 5am on Monday
    }
  }
}

For a demanding monorepo, confine its off-hours work through the MEND_RNV_CRON_JOB_SCHEDULER_* cron times, which is the reliable layer for timing once schedules are centrally governed.

7. Scaling with Enterprise Workers and Queues

The Enterprise architecture is the primary mechanism for scaling monorepo processing. It separates the Server (scheduler, priority job queue, webhook handler, API) from the Workers (which each pull the highest-priority job and run a scan on one repository). Because these components scale independently, you can process many repositories in parallel and isolate a demanding monorepo so that it does not block the rest of your fleet. Every setting in this section is configured by Renovate administrators through server and worker environment variables (the MEND_RNV_* settings) and the bot configuration, not in repository config.

Worker Replicas

Horizontal scaling is achieved by increasing the number of worker replicas. Additional workers allow more repositories, including your large monorepo, to be processed concurrently rather than serially.

Dedicated Worker Queues

The MEND_RNV_WORKER_QUEUES setting (default main) routes repositories to named queues and dedicates worker pools to them. Assign the monorepo to its own queue and provision workers that consume it. A long monorepo scan then runs on a dedicated pool and does not delay the queue that serves your smaller repositories.

Queue assignment follows a precedence of repository over organization over the default main, allowing you to target a single monorepo precisely. A worker can be configured to consume all queues when general capacity is required. Queues are created and repositories assigned to them through the Enterprise System API (POST /system/v1/queues to create a queue, PATCH /system/v1/repos/{org/repo}/queue to assign a repository), which requires the API to be enabled with MEND_RNV_API_ENABLED and MEND_RNV_API_ENABLE_SYSTEM.

Typical pattern:

  • A monorepo queue with a small pool of larger, well-resourced workers.

  • The default main queue with a larger pool of standard workers for everything else.

Execution Timeouts

MEND_RNV_WORKER_EXECUTION_TIMEOUT (default 60 minutes) caps how long a single scan may run. A very large monorepo with heavy lockfile regeneration may legitimately require more time, so increase this value deliberately for the pool that serves it. On GitHub, increasing it beyond 60 minutes has no practical effect, because the GitHub App token expires at the one-hour mark regardless (see Important Limitation When Integrating with GitHub in section 1). A job that exceeds the limit is stopped and the repository is given a timeout status, which the weekly capped scheduler (MEND_RNV_CRON_JOB_SCHEDULER_CAPPED) then handles. Treat that as a signal to further scope or split the work.

A second, less visible timeout applies inside the scan. Renovate terminates any single child process, such as one pnpm install, after the bot-level executionTimeout (default 15 minutes). A monorepo whose install alone approaches that limit fails on the child-process timeout while still well under the worker limit, so raise executionTimeout alongside MEND_RNV_WORKER_EXECUTION_TIMEOUT.

Tiered Scheduling

Enterprise runs each repository on one of several cron tiers (MEND_RNV_CRON_JOB_SCHEDULER_HOT, _COLD, _CAPPED, and _ALL) so that active repositories are scanned more often than idle ones. The tiers map to repository statuses. HOT (default hourly) covers new and activated repositories, where activated means at least one Renovate pull request has been merged. COLD (default daily) covers onboarding, onboarded, and failed. CAPPED (default weekly) covers resource-limit and timeout, so a repository that hits a resource limit or times out is demoted to this tier. ALL (default monthly) covers every enabled repository. Tune these cadences so a demanding monorepo is scanned as often as it needs without starving the rest of the fleet. The full model, including how repositories move between tiers, is documented in Renovate EE job processing.

Webhooks and Queue Priority

The server's job queue is priority-ordered, and jobs triggered by webhook events, such as a push that changes the Renovate configuration, are enqueued ahead of scheduler-enqueued jobs. A busy monorepo can therefore trigger scans outside its cron tier. Two settings widen which branch events are considered. MEND_RNV_WEBHOOK_BASE_BRANCHES adds branch names to the default base branch list (master, main, develop), and MEND_RNV_WEBHOOK_BRANCH_PREFIXES adds prefixes to the default Renovate branch prefix list (renovate/). Set them when your monorepo's release branches or a custom branchPrefix fall outside those defaults, so their webhook events still enqueue jobs. Separately, MEND_RNV_ENQUEUE_JOBS_ON_STARTUP (default discovered, which enqueues jobs only for newly discovered repositories) governs what is queued when the server restarts, and prevents a restart from flooding the queue ahead of a heavy monorepo job.

Worker Resource Allocation

  • MEND_RNV_NODE_OPTIONS sets NODE_OPTIONS on the worker. Raising --max-old-space-size provides large extractions with additional memory.

  • MEND_RNV_WORKER_CLEANUP is off by default. Set it to always or a cron expression to periodically clear the worker cache directories so that a monorepo worker does not exhaust its disk over time. Validate the cron value, because an invalid expression shuts the service down. MEND_RNV_WORKER_CLEANUP_DIRS customizes which directories are cleared, and note that setting it replaces the default list rather than adding to it.

Queue Monitoring

Enable Prometheus metrics (MEND_RNV_API_ENABLE_PROMETHEUS_METRICS) to expose queue backlog and wait times. These metrics indicate when to scale worker replicas up or down and confirm that your monorepo queue is keeping pace.

Diagnosing a Slow Scan

Before tuning timeouts or adding workers, find out where a monorepo scan actually spends its time. Persist job logs with MEND_RNV_LOG_HISTORY_DIR (or MEND_RNV_LOG_HISTORY_S3), then analyze them with your existing log analysis tooling. The Enterprise Web UI is also an option, offering per-repository browsing with log-level filtering and full-log download. For a deeper look at a single run, raise verbosity with LOG_LEVEL=debug. The log shows whether the time goes to extraction, dependency lookups, or lockfile regeneration, which tells you whether the right fix is scoping, caching, or a larger worker.

Summary

Handling a large monorepo in Renovate Enterprise rests on two principles, performing less work per run and distributing the work that remains. Onboard the repository deliberately so the initial backlog arrives in batches you control, scope the scan so Renovate parses only what matters, cache aggressively so repeat runs and datasource lookups stay cheap, keep lockfile regeneration and pull request volume under deliberate control, automerge the update classes that need no human review, and use the Enterprise worker and queue model to give a demanding monorepo dedicated capacity. Applied together, these practices let even a very large monorepo be processed predictably without starving the rest of your fleet.