diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md
index d0d5738c2140b2fcae50028a2a19b36fe1887ff7..c10ce7dec5ec7cc33fb4e9c5b771c8fc52013495 100644
--- a/docs/usage/configuration-options.md
+++ b/docs/usage/configuration-options.md
@@ -206,6 +206,23 @@ If so then Renovate will reflect this setting in its description and use package
 
 Configuring this to `true` means that Renovate will detect and apply the default reviewers rules to PRs (Bitbucket only).
 
+## branchConcurrentLimit
+
+By default, Renovate won't enforce any concurrent branch limits. If you want the same limit for both concurrent branches
+and concurrent PRs, then just set a value for `prConcurrentLimit` and it will be reused for branch calculations too.
+However, if you want to allow more concurrent branches than concurrent PRs, you can configure both values (
+e.g. `branchConcurrentLimit=5` and `prConcurrentLimit=3`).
+
+This limit is enforced on a per-repository basis.
+
+Example config:
+
+```json
+{
+  "branchConcurrentLimit": 3
+}
+```
+
 ## branchName
 
 Warning: it's strongly recommended not to configure this field directly.
diff --git a/lib/config/definitions.ts b/lib/config/definitions.ts
index b705862c1a963b40adfa5965fdb6b1ae20e647db..f4c66a57d99fcabe671dba8ffb3261759c503816 100644
--- a/lib/config/definitions.ts
+++ b/lib/config/definitions.ts
@@ -1186,6 +1186,13 @@ const options: RenovateOptions[] = [
     type: 'integer',
     default: 0, // no limit
   },
+  {
+    name: 'branchConcurrentLimit',
+    description:
+      'Limit to a maximum of x concurrent branches. 0 means no limit, `null` (default) inherits value from `prConcurrentLimit`.',
+    type: 'integer',
+    default: null, // inherit prConcurrentLimit
+  },
   {
     name: 'prPriority',
     description:
diff --git a/lib/workers/branch/index.spec.ts b/lib/workers/branch/index.spec.ts
index a1c920a41bec671c117b2452dda1659dc8b22c47..d73f0ca77dcdceed7530a248d903cbe3a8a465fe 100644
--- a/lib/workers/branch/index.spec.ts
+++ b/lib/workers/branch/index.spec.ts
@@ -216,7 +216,7 @@ describe('workers/branch', () => {
       const res = await branchWorker.processBranch(config);
       expect(res).toEqual(ProcessBranchResult.PrEdited);
     });
-    it('returns if pr creation limit exceeded', async () => {
+    it('returns if branch creation limit exceeded', async () => {
       getUpdated.getUpdatedPackageFiles.mockResolvedValueOnce({
         ...updatedPackageFiles,
       });
@@ -227,7 +227,7 @@ describe('workers/branch', () => {
       limits.isLimitReached.mockReturnValueOnce(true);
       limits.isLimitReached.mockReturnValueOnce(false);
       expect(await branchWorker.processBranch(config)).toEqual(
-        ProcessBranchResult.PrLimitReached
+        ProcessBranchResult.BranchLimitReached
       );
     });
     it('returns if pr creation limit exceeded and branch exists', async () => {
diff --git a/lib/workers/branch/index.ts b/lib/workers/branch/index.ts
index ba15f4943e580f6eac61a474cca22946a01f77ec..8a46fe8da5ef5f164ec88a0dc09d0096b1c2d790 100644
--- a/lib/workers/branch/index.ts
+++ b/lib/workers/branch/index.ts
@@ -152,12 +152,12 @@ export async function processBranch(
     }
     if (
       !branchExists &&
-      isLimitReached(Limit.PullRequests) &&
+      isLimitReached(Limit.Branches) &&
       !dependencyDashboardCheck &&
       !config.vulnerabilityAlert
     ) {
-      logger.debug('Reached PR limit - skipping branch creation');
-      return ProcessBranchResult.PrLimitReached;
+      logger.debug('Reached branch limit - skipping branch creation');
+      return ProcessBranchResult.BranchLimitReached;
     }
     if (
       isLimitReached(Limit.Commits) &&
diff --git a/lib/workers/common.ts b/lib/workers/common.ts
index bc378100beb4727f5fc19d8bd7144a500bd5be22..ae41f5553623ed164a994f9d2231bf92b634f298 100644
--- a/lib/workers/common.ts
+++ b/lib/workers/common.ts
@@ -94,6 +94,7 @@ export enum ProcessBranchResult {
   PrEdited = 'pr-edited',
   PrLimitReached = 'pr-limit-reached',
   CommitLimitReached = 'commit-limit-reached',
+  BranchLimitReached = 'branch-limit-reached',
   Rebase = 'rebase',
 }
 
diff --git a/lib/workers/global/limits.ts b/lib/workers/global/limits.ts
index c5476f8922fb81b7c78843675fc278233142bc88..01c80148bc071efbff6c293a85b529654704f4df 100644
--- a/lib/workers/global/limits.ts
+++ b/lib/workers/global/limits.ts
@@ -3,6 +3,7 @@ import { logger } from '../../logger';
 export enum Limit {
   Commits = 'Commits',
   PullRequests = 'PullRequests',
+  Branches = 'Branches',
 }
 
 interface LimitValue {
diff --git a/lib/workers/pr/index.ts b/lib/workers/pr/index.ts
index 6adc52e4eb1907d4fb673962fe9065b7b15fe4a1..2a6b63c3cda16ed9959df717f4715d4e319d90fc 100644
--- a/lib/workers/pr/index.ts
+++ b/lib/workers/pr/index.ts
@@ -16,7 +16,7 @@ import {
 } from '../../util/git';
 import * as template from '../../util/template';
 import { BranchConfig, PrResult } from '../common';
-import { Limit, isLimitReached } from '../global/limits';
+import { Limit, incLimitedValue, isLimitReached } from '../global/limits';
 import { getPrBody } from './body';
 import { ChangeLogError } from './changelog';
 import { codeOwnersForPr } from './code-owners';
@@ -396,6 +396,7 @@ export async function ensurePr(
           platformOptions: getPlatformPrOptions(config),
           draftPR: config.draftPR,
         });
+        incLimitedValue(Limit.PullRequests);
         logger.info({ pr: pr.number, prTitle }, 'PR created');
       }
     } catch (err) /* istanbul ignore next */ {
diff --git a/lib/workers/repository/dependency-dashboard.ts b/lib/workers/repository/dependency-dashboard.ts
index dc09aef5e39f294fd0641cc19951f3accf2aa491..0e546c7c00140e760087062334a91caa331a7092 100644
--- a/lib/workers/repository/dependency-dashboard.ts
+++ b/lib/workers/repository/dependency-dashboard.ts
@@ -119,6 +119,7 @@ export async function ensureMasterIssue(
   }
   const rateLimited = branches.filter(
     (branch) =>
+      branch.res === ProcessBranchResult.BranchLimitReached ||
       branch.res === ProcessBranchResult.PrLimitReached ||
       branch.res === ProcessBranchResult.CommitLimitReached
   );
@@ -185,6 +186,7 @@ export async function ensureMasterIssue(
     ProcessBranchResult.NotScheduled,
     ProcessBranchResult.PrLimitReached,
     ProcessBranchResult.CommitLimitReached,
+    ProcessBranchResult.BranchLimitReached,
     ProcessBranchResult.AlreadyExisted,
     ProcessBranchResult.Error,
     ProcessBranchResult.Automerged,
diff --git a/lib/workers/repository/process/limits.spec.ts b/lib/workers/repository/process/limits.spec.ts
index a268bec875ca18200a8583547a4f23d5bcfc311d..57045ba460d838850b1fa921fbacd34a6a31cc9f 100644
--- a/lib/workers/repository/process/limits.spec.ts
+++ b/lib/workers/repository/process/limits.spec.ts
@@ -1,9 +1,16 @@
 import { DateTime } from 'luxon';
-import { RenovateConfig, getConfig, platform } from '../../../../test/util';
+import {
+  RenovateConfig,
+  getConfig,
+  git,
+  platform,
+} from '../../../../test/util';
 import { PrState } from '../../../types';
 import { BranchConfig } from '../../common';
 import * as limits from './limits';
 
+jest.mock('../../../util/git');
+
 let config: RenovateConfig;
 beforeEach(() => {
   jest.resetAllMocks();
@@ -82,4 +89,50 @@ describe('workers/repository/process/limits', () => {
       expect(res).toEqual(5);
     });
   });
+
+  describe('getConcurrentBranchesRemaining()', () => {
+    it('calculates concurrent limit remaining', () => {
+      config.branchConcurrentLimit = 20;
+      git.branchExists.mockReturnValueOnce(true);
+      const res = limits.getConcurrentBranchesRemaining(config, [
+        { branchName: 'foo' },
+      ] as never);
+      expect(res).toEqual(19);
+    });
+    it('defaults to prConcurrentLimit', () => {
+      config.branchConcurrentLimit = null;
+      config.prConcurrentLimit = 20;
+      git.branchExists.mockReturnValueOnce(true);
+      const res = limits.getConcurrentBranchesRemaining(config, [
+        { branchName: 'foo' },
+      ] as never);
+      expect(res).toEqual(19);
+    });
+    it('does not use prConcurrentLimit for explicit branchConcurrentLimit=0', () => {
+      config.branchConcurrentLimit = 0;
+      config.prConcurrentLimit = 20;
+      const res = limits.getConcurrentBranchesRemaining(config, []);
+      expect(res).toEqual(99);
+    });
+    it('returns 99 if no limits are set', () => {
+      const res = limits.getConcurrentBranchesRemaining(config, []);
+      expect(res).toEqual(99);
+    });
+    it('returns prConcurrentLimit if errored', () => {
+      config.branchConcurrentLimit = 2;
+      const res = limits.getConcurrentBranchesRemaining(config, null);
+      expect(res).toEqual(2);
+    });
+  });
+
+  describe('getBranchesRemaining()', () => {
+    it('returns concurrent branches', () => {
+      config.branchConcurrentLimit = 20;
+      git.branchExists.mockReturnValueOnce(true);
+      const res = limits.getBranchesRemaining(config, [
+        { branchName: 'foo' },
+      ] as never);
+      expect(res).toEqual(19);
+    });
+  });
 });
diff --git a/lib/workers/repository/process/limits.ts b/lib/workers/repository/process/limits.ts
index 88ba4b53e1d69bdaf86ddfd7f3104a9a9c8ec7f3..536c959a84db2f9dc2cb5962762c8fe8feb97270 100644
--- a/lib/workers/repository/process/limits.ts
+++ b/lib/workers/repository/process/limits.ts
@@ -4,6 +4,7 @@ import { logger } from '../../../logger';
 import { Pr, platform } from '../../../platform';
 import { PrState } from '../../../types';
 import { ExternalHostError } from '../../../types/errors/external-host-error';
+import { branchExists } from '../../../util/git';
 import { BranchConfig } from '../../common';
 
 export async function getPrHourlyRemaining(
@@ -84,3 +85,40 @@ export async function getPrsRemaining(
   const concurrentRemaining = await getConcurrentPrsRemaining(config, branches);
   return Math.min(hourlyRemaining, concurrentRemaining);
 }
+
+export function getConcurrentBranchesRemaining(
+  config: RenovateConfig,
+  branches: BranchConfig[]
+): number {
+  const { branchConcurrentLimit, prConcurrentLimit } = config;
+  const limit =
+    typeof branchConcurrentLimit === 'number'
+      ? branchConcurrentLimit
+      : prConcurrentLimit;
+  if (typeof limit === 'number' && limit) {
+    logger.debug(`Calculating branchConcurrentLimit (${limit})`);
+    try {
+      let currentlyOpen = 0;
+      for (const branch of branches) {
+        if (branchExists(branch.branchName)) {
+          currentlyOpen += 1;
+        }
+      }
+      logger.debug(`${currentlyOpen} branches are currently open`);
+      const concurrentRemaining = Math.max(0, limit - currentlyOpen);
+      logger.debug(`Branch concurrent limit remaining: ${concurrentRemaining}`);
+      return concurrentRemaining;
+    } catch (err) {
+      logger.error({ err }, 'Error checking concurrent branches');
+      return limit;
+    }
+  }
+  return 99;
+}
+
+export function getBranchesRemaining(
+  config: RenovateConfig,
+  branches: BranchConfig[]
+): number {
+  return getConcurrentBranchesRemaining(config, branches);
+}
diff --git a/lib/workers/repository/process/write.spec.ts b/lib/workers/repository/process/write.spec.ts
index 378544c3bb9f6f390d1d993f6fa8a2f26d4dfee1..c220580e2efa399b1a97a53baddc589700353b6a 100644
--- a/lib/workers/repository/process/write.spec.ts
+++ b/lib/workers/repository/process/write.spec.ts
@@ -13,6 +13,7 @@ const limits = mocked(_limits);
 branchWorker.processBranch = jest.fn();
 
 limits.getPrsRemaining = jest.fn().mockResolvedValue(99);
+limits.getBranchesRemaining = jest.fn().mockReturnValue(99);
 
 let config: RenovateConfig;
 beforeEach(() => {
@@ -61,14 +62,14 @@ describe('workers/repository/write', () => {
     it('increments branch counter', async () => {
       const branches: BranchConfig[] = [{}] as never;
       branchWorker.processBranch.mockResolvedValueOnce(
-        ProcessBranchResult.Pending
+        ProcessBranchResult.PrCreated
       );
       git.branchExists.mockReturnValueOnce(false);
       git.branchExists.mockReturnValueOnce(true);
-      limits.getPrsRemaining.mockResolvedValueOnce(1);
-      expect(isLimitReached(Limit.PullRequests)).toBeFalse();
+      limits.getBranchesRemaining.mockReturnValueOnce(1);
+      expect(isLimitReached(Limit.Branches)).toBeFalse();
       await writeUpdates({ config }, branches);
-      expect(isLimitReached(Limit.PullRequests)).toBeTrue();
+      expect(isLimitReached(Limit.Branches)).toBeTrue();
     });
   });
 });
diff --git a/lib/workers/repository/process/write.ts b/lib/workers/repository/process/write.ts
index 19c2bb98a7644ce9be22a7f3ac19bd9a23ca0f09..02e1e1b920957ee091e9197ab7af10e08b7ef601 100644
--- a/lib/workers/repository/process/write.ts
+++ b/lib/workers/repository/process/write.ts
@@ -1,24 +1,13 @@
 import { RenovateConfig } from '../../../config';
 import { addMeta, logger, removeMeta } from '../../../logger';
-import { platform } from '../../../platform';
-import { PrState } from '../../../types';
 import { branchExists } from '../../../util/git';
 import { processBranch } from '../../branch';
 import { BranchConfig, ProcessBranchResult } from '../../common';
 import { Limit, incLimitedValue, setMaxLimit } from '../../global/limits';
-import { getPrsRemaining } from './limits';
+import { getBranchesRemaining, getPrsRemaining } from './limits';
 
 export type WriteUpdateResult = 'done' | 'automerged';
 
-async function prExists(branchName: string): Promise<boolean> {
-  try {
-    const pr = await platform.getBranchPr(branchName);
-    return pr?.state === PrState.Open;
-  } catch (err) /* istanbul ignore next */ {
-    return false;
-  }
-}
-
 export async function writeUpdates(
   config: RenovateConfig,
   allBranches: BranchConfig[]
@@ -39,13 +28,21 @@ export async function writeUpdates(
     }
     return true;
   });
+
   const prsRemaining = await getPrsRemaining(config, branches);
   logger.debug({ prsRemaining }, 'Calculated maximum PRs remaining this run');
   setMaxLimit(Limit.PullRequests, prsRemaining);
+
+  const branchesRemaining = getBranchesRemaining(config, branches);
+  logger.debug(
+    { branchesRemaining },
+    'Calculated maximum branches remaining this run'
+  );
+  setMaxLimit(Limit.Branches, branchesRemaining);
+
   for (const branch of branches) {
     addMeta({ branch: branch.branchName });
     const branchExisted = branchExists(branch.branchName);
-    const prExisted = await prExists(branch.branchName);
     const res = await processBranch(branch);
     branch.res = res;
     if (
@@ -55,26 +52,8 @@ export async function writeUpdates(
       // Stop processing other branches because base branch has been changed
       return 'automerged';
     }
-    if (res === ProcessBranchResult.PrCreated) {
-      incLimitedValue(Limit.PullRequests);
-    }
-    // istanbul ignore if
-    else if (
-      res === ProcessBranchResult.Automerged &&
-      branch.automergeType === 'pr-comment' &&
-      branch.requiredStatusChecks === null
-    ) {
-      incLimitedValue(Limit.PullRequests);
-    } else if (
-      res === ProcessBranchResult.Pending &&
-      !branchExisted &&
-      branchExists(branch.branchName)
-    ) {
-      incLimitedValue(Limit.PullRequests);
-    }
-    // istanbul ignore if
-    else if (!prExisted && (await prExists(branch.branchName))) {
-      incLimitedValue(Limit.PullRequests);
+    if (!branchExisted && branchExists(branch.branchName)) {
+      incLimitedValue(Limit.Branches);
     }
   }
   removeMeta(['branch']);