diff --git a/packages/cloud/src/routes/tenants.test.ts b/packages/cloud/src/routes/tenants.test.ts
index 9ad2aca0a..7fcaf1b86 100644
--- a/packages/cloud/src/routes/tenants.test.ts
+++ b/packages/cloud/src/routes/tenants.test.ts
@@ -49,6 +49,26 @@ describe('POST /api/tenants', () => {
     ).rejects.toMatchObject({ status: 403 });
   });
 
+  it('should throw 403 when trying to create more than 3 tenants with `CreateTenant` scope', async () => {
+    const tenant: TenantInfo = {
+      id: 'tenant',
+      name: 'tenant',
+      tag: TenantTag.Development,
+      indicator: 'https://tenant.foo.bar',
+    };
+    library.getAvailableTenants.mockResolvedValueOnce([tenant, tenant, tenant]);
+
+    await expect(
+      router.routes()(
+        buildRequestAuthContext('POST /tenants', {
+          body: { name: 'tenant', tag: TenantTag.Development },
+        })([CloudScope.CreateTenant]),
+        noop,
+        createHttpContext()
+      )
+    ).rejects.toMatchObject({ status: 403 });
+  });
+
   it('should be able to create a new tenant', async () => {
     const tenant: TenantInfo = {
       id: 'tenant_a',
@@ -56,6 +76,7 @@ describe('POST /api/tenants', () => {
       tag: TenantTag.Development,
       indicator: 'https://foo.bar',
     };
+    library.getAvailableTenants.mockResolvedValueOnce([]);
     library.createNewTenant.mockImplementationOnce(async (_, payload) => {
       return { ...tenant, ...payload };
     });
diff --git a/packages/cloud/src/routes/tenants.ts b/packages/cloud/src/routes/tenants.ts
index 023d63d8d..d6f7dc684 100644
--- a/packages/cloud/src/routes/tenants.ts
+++ b/packages/cloud/src/routes/tenants.ts
@@ -68,6 +68,18 @@ export const tenantsRoutes = (library: TenantsLibrary) =>
           throw new RequestError('Forbidden due to lack of permission.', 403);
         }
 
+        /**
+         * Should throw 403 when users with `CreateTenant` scope are attempting to create more than 3 tenants.
+         * This does not apply to users with `ManageTenant` scope.
+         */
+        if (context.auth.scopes.includes(CloudScope.CreateTenant)) {
+          const availableTenants = await library.getAvailableTenants(context.auth.id);
+          assert(
+            availableTenants.length < 3,
+            new RequestError(`Can not have more than 3 tenants.`, 403)
+          );
+        }
+
         return next({
           ...context,
           json: await library.createNewTenant(context.auth.id, context.guarded.body),
diff --git a/packages/console/src/containers/AppContent/components/Topbar/TenantSelector/index.module.scss b/packages/console/src/containers/AppContent/components/Topbar/TenantSelector/index.module.scss
index 5f67b00fe..b3905640b 100644
--- a/packages/console/src/containers/AppContent/components/Topbar/TenantSelector/index.module.scss
+++ b/packages/console/src/containers/AppContent/components/Topbar/TenantSelector/index.module.scss
@@ -119,9 +119,24 @@
     cursor: pointer;
   }
 
-  .icon {
+  > svg {
     width: 20px;
     height: 20px;
     color: var(--color-neutral-50);
   }
+
+  &.disabled {
+    &:hover {
+      background: transparent;
+    }
+
+    &:not(:disabled) {
+      cursor: not-allowed;
+    }
+
+    > div,
+    > svg {
+      color: var(--color-placeholder);
+    }
+  }
 }
diff --git a/packages/console/src/containers/AppContent/components/Topbar/TenantSelector/index.tsx b/packages/console/src/containers/AppContent/components/Topbar/TenantSelector/index.tsx
index c0d263fc3..002dd76ef 100644
--- a/packages/console/src/containers/AppContent/components/Topbar/TenantSelector/index.tsx
+++ b/packages/console/src/containers/AppContent/components/Topbar/TenantSelector/index.tsx
@@ -1,4 +1,4 @@
-import { type TenantInfo, TenantTag } from '@logto/schemas/models';
+import { type TenantInfo } from '@logto/schemas/models';
 import classNames from 'classnames';
 import { useRef, useState } from 'react';
 import { toast } from 'react-hot-toast';
@@ -35,10 +35,12 @@ function TenantSelector() {
     return <AppError errorMessage={error.message} callStack={error.stack} />;
   }
 
-  if (!tenants?.length) {
+  if (!tenants?.length || !currentTenantInfo) {
     return null;
   }
 
+  const isCreateButtonDisabled = tenants.length >= 3;
+
   return (
     <>
       <div
@@ -53,11 +55,8 @@ function TenantSelector() {
           setShowDropdown(true);
         }}
       >
-        <div className={styles.name}>{currentTenantInfo?.name ?? 'My project'}</div>
-        <TenantEnvTag
-          className={styles.tag}
-          tag={currentTenantInfo?.tag ?? TenantTag.Development}
-        />
+        <div className={styles.name}>{currentTenantInfo.name}</div>
+        <TenantEnvTag className={styles.tag} tag={currentTenantInfo.tag} />
         <KeyboardArrowDown className={styles.arrowIcon} />
       </div>
       <Dropdown
@@ -89,16 +88,25 @@ function TenantSelector() {
         <div
           role="button"
           tabIndex={0}
-          className={styles.createTenantButton}
+          className={classNames(
+            isCreateButtonDisabled && styles.disabled,
+            styles.createTenantButton
+          )}
           onClick={() => {
+            if (isCreateButtonDisabled) {
+              return;
+            }
             setShowCreateTenantModal(true);
           }}
           onKeyDown={onKeyDownHandler(() => {
+            if (isCreateButtonDisabled) {
+              return;
+            }
             setShowCreateTenantModal(true);
           })}
         >
           <div>{t('cloud.tenant.create_tenant')}</div>
-          <PlusSign className={styles.icon} />
+          <PlusSign />
         </div>
       </Dropdown>
       <CreateTenantModal
diff --git a/packages/integration-tests/src/tests/api-cloud/tenant.test.ts b/packages/integration-tests/src/tests/api-cloud/tenant.test.ts
index 85c3de0cc..2b6a11fb6 100644
--- a/packages/integration-tests/src/tests/api-cloud/tenant.test.ts
+++ b/packages/integration-tests/src/tests/api-cloud/tenant.test.ts
@@ -85,6 +85,9 @@ describe('Tenant APIs', () => {
       expect(tenant).toHaveProperty('tag', payload.tag);
       expect(tenant).toHaveProperty('name', payload.name);
     }
+    await expect(
+      createTenant(accessToken, { name: 'tenant4', tag: TenantTag.Staging })
+    ).rejects.toThrow();
     await deleteTenant(accessToken, tenant3.id);
     const resources = await authedAdminTenantApi.get('resources').json<Resource[]>();
     expect(