diff --git a/config/config.sample.yaml b/config/config.sample.yaml
index e884b4888bb14602a09ef3d46a136ec6e4f9d7c2..edb0b2e6994a21f6a89afdd6996173cfd7512a19 100644
--- a/config/config.sample.yaml
+++ b/config/config.sample.yaml
@@ -23,6 +23,9 @@ bridge:
   disableTypingNotifications: false
   # Disable deleting messages on Discord if a message is redacted on Matrix.
   disableDeletionForwarding: false
+  # Disable portal bridging, where Matrix users can search for unbridged Discord
+  # rooms on their Matrix server.
+  disablePortalBridging: false
   # Enable users to bridge rooms using !discord commands. See
   # https://t2bot.io/discord for instructions.
   enableSelfServiceBridging: false
@@ -99,6 +102,8 @@ limits:
     # echos = (Copies of a sent message may arrive from discord before we've
     # fininished handling it, causing us to echo it back to the room)
     discordSendDelay: 1500
+    # Set a maximum of rooms to be bridged.
+    # roomCount: 20
 ghosts:
     # Pattern for the ghosts nick, available is :nick, :username, :tag and :id
     nickPattern: ":nick"
diff --git a/config/config.schema.yaml b/config/config.schema.yaml
index cff80713089f0405c5c84dd611cfbe04338516c4..112c85019cbc7968a3b09a5b6b0cf3d2698d70be 100644
--- a/config/config.schema.yaml
+++ b/config/config.schema.yaml
@@ -20,6 +20,8 @@ properties:
             type: "boolean"
           disableDeletionForwarding:
             type: "boolean"
+          disablePortalBridging:
+            type: "boolean"
           enableSelfServiceBridging:
             type: "boolean"
           disableReadReceipts:
@@ -95,6 +97,8 @@ properties:
                 type: "number"
             discordSendDelay:
                 type: "number"
+            roomCount:
+                type: "number"
     channel:
         type: "object"
         properties:
diff --git a/src/config.ts b/src/config.ts
index 525fad298efd43be8b47797413e143fb64f30e83..256189dc161687838922d5936ba4ec074c0db0ed 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -89,6 +89,7 @@ class DiscordBridgeConfigBridge {
     public disableDiscordMentions: boolean;
     public disableDeletionForwarding: boolean;
     public enableSelfServiceBridging: boolean;
+    public disablePortalBridging: boolean;
     public disableReadReceipts: boolean;
     public disableEveryoneMention: boolean = false;
     public disableHereMention: boolean = false;
@@ -140,6 +141,7 @@ export class DiscordBridgeConfigChannelDeleteOptions {
 class DiscordBridgeConfigLimits {
     public roomGhostJoinDelay: number = 6000;
     public discordSendDelay: number = 1500;
+    public roomCount: number = -1;
 }
 
 export class LoggingFile {
diff --git a/src/db/roomstore.ts b/src/db/roomstore.ts
index 4b526c7c392a7eef5e0d0f43a7903faf52f16060..401e29e7451cc5aef67efea5dcf4da0794776246 100644
--- a/src/db/roomstore.ts
+++ b/src/db/roomstore.ts
@@ -98,6 +98,29 @@ export class DbRoomStore {
         this.entriesMatrixIdCache = new TimedCache(ENTRY_CACHE_LIMETIME);
     }
 
+    /**
+     * Returns the number of bridged room pairs. Every connection between a
+     * Matrix room and a remote room counts as one pair.
+     * @returns {number} The amount of room pairs as an integer
+     */
+    public async countEntries(): Promise<number> {
+        const row = (await this.db.Get("SELECT COUNT(*) AS count FROM room_entries WHERE matrix_id IS NOT NULL AND remote_id IS NOT NULL")) || {};
+
+        // Our Sqlite wrapper returns a number – which is what we want.
+        let count = row.count;
+        // Our PostgreSQL wrapper returns a string.
+        if (typeof count === 'string') {
+            count = Number.parseInt(count);
+        }
+
+        if (typeof count !== "number") {
+            log.error("Failed to count room entries");
+            throw Error(`Failed to count room entries ${JSON.stringify(row)} AND ${typeof count}`);
+        }
+
+        return count;
+    }
+
     public async upsertEntry(entry: IRoomStoreEntry) {
         const promises: Promise<void>[] = [];
 
diff --git a/src/discordas.ts b/src/discordas.ts
index 930e20349bc729bc424c4a14fe6cdf58bfaa176e..c8267cb4844dcbd9fc4f2d6e81e3ce75a2fbb035 100644
--- a/src/discordas.ts
+++ b/src/discordas.ts
@@ -187,17 +187,19 @@ async function run(): Promise<void> {
     appservice.expressAppInstance.get("/health", (_, res: Response) => {
         res.status(200).send("");
     });
-    
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    appservice.on("query.room", async (roomAlias: string, createRoom: (opts: any) => Promise<void>) => {
-        try {
-            const createRoomOpts = await roomhandler.OnAliasQuery(roomAlias);
-            await createRoom(createRoomOpts);
-            await roomhandler.OnAliasQueried(roomAlias, createRoomOpts.__roomId);
-        } catch (err) {
-            log.error("Exception thrown while handling \"query.room\" event", err);
-        }
-    });
+
+    if (config.bridge.disablePortalBridging !== true) {
+        // eslint-disable-next-line @typescript-eslint/no-explicit-any
+        appservice.on("query.room", async (roomAlias: string, createRoom: (opts: any) => Promise<void>) => {
+            try {
+                const createRoomOpts = await roomhandler.OnAliasQuery(roomAlias);
+                await createRoom(createRoomOpts);
+                await roomhandler.OnAliasQueried(roomAlias, createRoomOpts.__roomId);
+            } catch (err) {
+                log.error("Exception thrown while handling \"query.room\" event", err);
+            }
+        });
+    }
 
     appservice.on("room.event", async (roomId: string, event: IMatrixEvent) => {
         try {
diff --git a/src/matrixcommandhandler.ts b/src/matrixcommandhandler.ts
index 08bcb9fb8c8d5a4fa676faa3f9bf8c6c73378cf9..5dfc306e7c506c27030c1239d8a62af0304d9103 100644
--- a/src/matrixcommandhandler.ts
+++ b/src/matrixcommandhandler.ts
@@ -82,6 +82,10 @@ export class MatrixCommandHandler {
                     if (!guildId || !channelId) {
                         return "Invalid syntax. For more information try `!discord help bridge`";
                     }
+                    if (await this.provisioner.RoomCountLimitReached(this.config.limits.roomCount)) {
+                        log.info(`Room count limit (value: ${this.config.limits.roomCount}) reached: Rejecting command to bridge new matrix room ${event.room_id} to ${guildId}/${channelId}`);
+                        return `This bridge has reached its room limit of ${this.config.limits.roomCount}. Unbridge another room to allow for new connections.`;
+                    }
                     try {
                         const discordResult = await this.discord.LookupRoom(guildId, channelId);
                         const channel = discordResult.channel as Discord.TextChannel;
diff --git a/src/provisioner.ts b/src/provisioner.ts
index 633c4c908e27cceb5ec3eee254792fc707ca8821..c1568af96f75c279cd8db257979a2c87e91932dd 100644
--- a/src/provisioner.ts
+++ b/src/provisioner.ts
@@ -40,6 +40,15 @@ export class Provisioner {
         return this.roomStore.linkRooms(local, remote);
     }
 
+    /**
+     * Returns if the room count limit has been reached.
+     * This can be set by the bridge admin and prevents new rooms from being bridged.
+     * @returns Has the limit been reached?
+     */
+    public async RoomCountLimitReached(limit: number): Promise<boolean> {
+        return limit >= 0 && await this.roomStore.countEntries() >= limit;
+    }
+
     public async UnbridgeChannel(channel: Discord.TextChannel, rId?: string) {
         const roomsRes = await this.roomStore.getEntriesByRemoteRoomData({
             discord_channel: channel.id,
diff --git a/test/db/test_roomstore.ts b/test/db/test_roomstore.ts
index f9b0b0fe08c4820578070eb616ebfb64cd28416f..9263391972bdb845341c920b431669dcf325ac2b 100644
--- a/test/db/test_roomstore.ts
+++ b/test/db/test_roomstore.ts
@@ -24,7 +24,7 @@ import { RemoteStoreRoom, MatrixStoreRoom } from "../../src/db/roomstore";
 
 let store: DiscordStore;
 describe("RoomStore", () => {
-    before(async () => {
+    beforeEach(async () => {
         store = new DiscordStore(":memory:");
         await store.init();
     });
@@ -189,4 +189,55 @@ describe("RoomStore", () => {
             expect(entries).to.be.empty;
         });
     });
+    describe("countEntries", () => {
+        it("returns 0 when no entry has been upserted", async () => {
+            expect(await store.roomStore.countEntries()).to.equal(0);
+        });
+        it("returns 1 when one entry has been upserted", async () => {
+            await store.roomStore.upsertEntry({
+                id: "test",
+                matrix: new MatrixStoreRoom("test_m"),
+                remote: new RemoteStoreRoom("test_r", { discord_guild: "find", discord_channel: "this" }),
+            });
+            expect(await store.roomStore.countEntries()).to.equal(1);
+        });
+        it("returns 2 when two entries have been upserted", async () => {
+            await store.roomStore.upsertEntry({
+                id: "test1",
+                matrix: new MatrixStoreRoom("test1_m"),
+                remote: new RemoteStoreRoom("test1_r", { discord_guild: "find", discord_channel: "this" }),
+            });
+            await store.roomStore.upsertEntry({
+                id: "test2",
+                matrix: new MatrixStoreRoom("test2_m"),
+                remote: new RemoteStoreRoom("test2_r", { discord_guild: "find", discord_channel: "this" }),
+            });
+            expect(await store.roomStore.countEntries()).to.equal(2);
+        });
+        it("does not count entries with no matrix_id", async () => {
+            await store.roomStore.upsertEntry({
+                id: "test",
+                matrix: null,
+                remote: new RemoteStoreRoom("test_r", { discord_guild: "find", discord_channel: "this" }),
+            });
+            expect(await store.roomStore.countEntries()).to.equal(0);
+        });
+        it("does not count entries with no remote_id", async () => {
+            await store.roomStore.upsertEntry({
+                id: "test",
+                matrix: new MatrixStoreRoom("test_m"),
+                remote: null,
+            });
+            expect(await store.roomStore.countEntries()).to.equal(0);
+        });
+        it("returns 0 when one entry has been upserted and removed", async () => {
+            await store.roomStore.upsertEntry({
+                id: "test",
+                matrix: new MatrixStoreRoom("test_m"),
+                remote: new RemoteStoreRoom("test_r", { discord_guild: "find", discord_channel: "this" }),
+            });
+            await store.roomStore.removeEntriesByRemoteRoomId("test_r");
+            expect(await store.roomStore.countEntries()).to.equal(0);
+        });
+    });
 });
diff --git a/test/test_matrixcommandhandler.ts b/test/test_matrixcommandhandler.ts
index 7f487f0a78d32b8c9225aef3b86f08aa8c3d74f5..2b74a8300756c39c1fa5beb341df38685262dee4 100644
--- a/test/test_matrixcommandhandler.ts
+++ b/test/test_matrixcommandhandler.ts
@@ -48,6 +48,9 @@ function createCH(opts: any = {}, shouldBeJoined = true) {
                 throw new Error("Test failed matrix bridge");
             }
         },
+        RoomCountLimitReached: async () => {
+            return !!opts.roomCountLimitReached;
+        },
         UnbridgeChannel: async () => {
             if (opts.failUnbridge) {
                 throw new Error("Test failed unbridge");
diff --git a/test/test_provisioner.ts b/test/test_provisioner.ts
index 9d09939a0f25b7797d83413cfd1db7ec8d3f9ad3..e8d38167db74c5621b38867c08e857fa5c2ca719 100644
--- a/test/test_provisioner.ts
+++ b/test/test_provisioner.ts
@@ -71,4 +71,30 @@ describe("Provisioner", () => {
             expect(await promise).to.eq("Approved");
         });
     });
+    describe("RoomCountLimitReached", () => {
+        it("should return false if no limit is defined", async () => {
+            const p = new Provisioner({
+                countEntries: async () => 7,
+            } as any, {} as any);
+            expect(await p.RoomCountLimitReached(-1)).to.equal(false);
+        });
+        it("should return false if less rooms exist than the limit", async () => {
+            const p = new Provisioner({
+                countEntries: async () => 7,
+            } as any, {} as any);
+            expect(await p.RoomCountLimitReached(10)).to.equal(false);
+        });
+        it("should return true if more rooms exist than the limit", async () => {
+            const p = new Provisioner({
+                countEntries: async () => 7,
+            } as any, {} as any);
+            expect(await p.RoomCountLimitReached(5)).to.equal(true);
+        });
+        it("should return true if there are as many rooms as the limit allows", async () => {
+            const p = new Provisioner({
+                countEntries: async () => 7,
+            } as any, {} as any);
+            expect(await p.RoomCountLimitReached(7)).to.equal(true);
+        });
+    });
 });