diff --git a/app/models/users/group_callout.rb b/app/models/users/group_callout.rb
index 3e3e424e9c94a88845b27412c7c09b45b90ad3ba..2552407fa4cda8f4d95edb1ac05af12b422b06df 100644
--- a/app/models/users/group_callout.rb
+++ b/app/models/users/group_callout.rb
@@ -23,7 +23,8 @@ class GroupCallout < ApplicationRecord
       namespace_storage_limit_banner_alert_threshold: 12, # EE-only
       namespace_storage_limit_banner_error_threshold: 13, # EE-only
       usage_quota_trial_alert: 14, # EE-only
-      preview_usage_quota_free_plan_alert: 15 # EE-only
+      preview_usage_quota_free_plan_alert: 15, # EE-only
+      enforcement_at_limit_alert: 16 # EE-only
     }
 
     validates :group, presence: true
diff --git a/ee/app/components/namespaces/free_user_cap/enforcement_at_limit_alert_component.rb b/ee/app/components/namespaces/free_user_cap/enforcement_at_limit_alert_component.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f55ba9cf15c5f231b99f66b7203d088c6b7b5e95
--- /dev/null
+++ b/ee/app/components/namespaces/free_user_cap/enforcement_at_limit_alert_component.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Namespaces
+  module FreeUserCap
+    class EnforcementAtLimitAlertComponent < BaseAlertComponent
+      private
+
+      ENFORCEMENT_AT_LIMIT_ALERT = 'enforcement_at_limit_alert'
+
+      def breached_cap_limit?
+        Shared.enforcement_at_limit?(namespace)
+      end
+
+      def feature_name
+        ENFORCEMENT_AT_LIMIT_ALERT
+      end
+
+      def alert_attributes
+        {
+          # see issue with ViewComponent overriding Kernel version
+          # https://github.com/github/view_component/issues/156#issuecomment-737469885
+          title: Kernel.format(
+            _("Your namespace %{namespace_name} has reached the %{free_limit} user limit"),
+            free_limit: free_user_limit,
+            namespace_name: namespace.name
+          ).html_safe,
+          body: Kernel.format(
+            n_("To invite more users, you can reduce the number of users in your " \
+               "namespace to %{free_limit} user or less. You can also upgrade to " \
+               "a paid tier which do not have user limits. If you need additional " \
+               "time, you can start a free 30-day trial which includes unlimited users.",
+               "To invite more users, you can reduce the number of users in your " \
+               "namespace to %{free_limit} users or less. You can also upgrade to " \
+               "a paid tier which do not have user limits. If you need additional " \
+               "time, you can start a free 30-day trial which includes unlimited users.",
+              free_user_limit
+            ),
+            free_limit: free_user_limit
+          ).html_safe,
+          primary_cta: namespace_primary_cta,
+          secondary_cta: namespace_secondary_cta
+        }
+      end
+    end
+  end
+end
diff --git a/ee/app/components/namespaces/free_user_cap/shared.rb b/ee/app/components/namespaces/free_user_cap/shared.rb
index 7b1178c1f643c114bec35c6eaac5250a95971233..0fa501819c39411a2b1dded8bf6fa396ae3a47f0 100644
--- a/ee/app/components/namespaces/free_user_cap/shared.rb
+++ b/ee/app/components/namespaces/free_user_cap/shared.rb
@@ -33,6 +33,10 @@ def self.close_button_data
         }
       end
 
+      def self.enforcement_at_limit?(namespace)
+        ::Namespaces::FreeUserCap::Enforcement.new(namespace).at_limit?
+      end
+
       def self.enforcement_over_limit?(namespace)
         ::Namespaces::FreeUserCap::Enforcement.new(namespace).over_limit?
       end
diff --git a/ee/app/models/namespaces/free_user_cap/enforcement.rb b/ee/app/models/namespaces/free_user_cap/enforcement.rb
index 9904eac0dcdc7d6f28d4ac1f3e1a3b8dfa6692d2..05dbbec45d3010fd7138d59eecb9645dcaa670fc 100644
--- a/ee/app/models/namespaces/free_user_cap/enforcement.rb
+++ b/ee/app/models/namespaces/free_user_cap/enforcement.rb
@@ -20,6 +20,13 @@ def reached_limit?
         users_count >= limit
       end
 
+      def at_limit?
+        return false unless enforce_cap?
+        return false unless new_namespace_enforcement?
+
+        users_count == limit
+      end
+
       def seat_available?(user)
         return true unless enforce_cap?
         return true if member_with_user_already_exists?(user)
diff --git a/ee/app/views/shared/_free_user_cap_alert.html.haml b/ee/app/views/shared/_free_user_cap_alert.html.haml
index 8d0fd9ec7286346a697c56203b7ec727606589fc..67c53a1b0ba7f0fec798dae1abd2733520fba2ee 100644
--- a/ee/app/views/shared/_free_user_cap_alert.html.haml
+++ b/ee/app/views/shared/_free_user_cap_alert.html.haml
@@ -3,6 +3,10 @@
     user: current_user,
     content_class: full_content_class)
 
+  = render Namespaces::FreeUserCap::EnforcementAtLimitAlertComponent.new(namespace: source.root_ancestor,
+    user: current_user,
+    content_class: full_content_class)
+
   = render Namespaces::FreeUserCap::NonOwnerAlertComponent.new(namespace: source.root_ancestor,
     user: current_user,
     content_class: full_content_class)
diff --git a/ee/spec/components/namespaces/free_user_cap/enforcement_at_limit_alert_component_spec.rb b/ee/spec/components/namespaces/free_user_cap/enforcement_at_limit_alert_component_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..aa3ffe1b3c073da140ca4af7f585cf02b18677e5
--- /dev/null
+++ b/ee/spec/components/namespaces/free_user_cap/enforcement_at_limit_alert_component_spec.rb
@@ -0,0 +1,115 @@
+# frozen_string_literal: true
+require "spec_helper"
+
+RSpec.describe Namespaces::FreeUserCap::EnforcementAtLimitAlertComponent,
+  :saas, :aggregate_failures, type: :component do
+  let_it_be(:namespace) { build_stubbed(:group) }
+  let_it_be(:content_class) { '_content_class_' }
+
+  let(:user) { build_stubbed(:user) }
+  let(:free_user_cap_at_limit?) { true }
+  let(:owner_access?) { true }
+
+  let_it_be(:title) do
+    "Your namespace #{namespace.name} has reached the " \
+    "#{::Namespaces::FreeUserCap.dashboard_limit} user limit"
+  end
+
+  subject(:component) do
+    described_class.new(namespace: namespace, user: user, content_class: content_class)
+  end
+
+  before do
+    allow_next_instance_of(::Namespaces::FreeUserCap::Enforcement) do |free_user_cap|
+      allow(free_user_cap).to receive(:at_limit?).and_return(free_user_cap_at_limit?)
+    end
+
+    allow(Ability).to receive(:allowed?)
+                  .with(user, :owner_access, namespace)
+                  .and_return(owner_access?)
+  end
+
+  context 'when user is authorized to see alert' do
+    context 'when at the limit' do
+      it 'has content for the alert' do
+        render_inline(component)
+
+        expect(page).to have_content(title)
+        expect(page).to have_css('.gl-alert-actions')
+        expect(page).to have_link('Manage members', href: group_usage_quotas_path(namespace))
+
+        expect(page).to have_link(
+          'Explore paid plans',
+          href: group_billings_path(namespace, source: 'user-limit-alert-enforcement')
+        )
+
+        expect(page).to have_css(".gl-overflow-auto.#{content_class}")
+
+        expect(page)
+          .to have_css("[data-testid='user-over-limit-free-plan-alert']" \
+                       "[data-dismiss-endpoint='#{group_callouts_path}']" \
+                       "[data-feature-id='#{described_class::ENFORCEMENT_AT_LIMIT_ALERT}']" \
+                       "[data-group-id='#{namespace.id}']")
+      end
+
+      it 'renders all the expected tracking items' do
+        render_inline(component)
+
+        expect(page).to have_css('.js-user-over-limit-free-plan-alert[data-track-action="render"]' \
+                                 '[data-track-label="user_limit_banner"]')
+        expect(page).to have_css('[data-testid="user-over-limit-primary-cta"]' \
+                                 '[data-track-action="click_button"]' \
+                                 '[data-track-label="manage_members"]')
+        expect(page).to have_css('[data-testid="user-over-limit-secondary-cta"]' \
+                                 '[data-track-action="click_button"]' \
+                                 '[data-track-label="explore_paid_plans"]')
+      end
+
+      context 'when alert has been dismissed' do
+        before do
+          allow(user).to receive(:dismissed_callout_for_group?).with(
+            feature_name: described_class::ENFORCEMENT_AT_LIMIT_ALERT,
+            group: namespace,
+            ignore_dismissal_earlier_than: nil
+          ).and_return(true)
+        end
+
+        it 'does not render the alert' do
+          render_inline(component)
+
+          expect(page).not_to have_content(title)
+        end
+      end
+    end
+
+    context 'when limit has not been reached' do
+      let(:free_user_cap_at_limit?) { false }
+
+      it 'does not render the alert' do
+        render_inline(component)
+
+        expect(page).not_to have_content(title)
+      end
+    end
+  end
+
+  context 'when user is not authorized to see alert' do
+    let(:owner_access?) { false }
+
+    it 'does not render the alert' do
+      render_inline(component)
+
+      expect(page).not_to have_content(title)
+    end
+  end
+
+  context 'when user does not exist' do
+    let(:user) { nil }
+
+    it 'does not render the alert' do
+      render_inline(component)
+
+      expect(page).not_to have_content(title)
+    end
+  end
+end
diff --git a/ee/spec/models/namespaces/free_user_cap/enforcement_spec.rb b/ee/spec/models/namespaces/free_user_cap/enforcement_spec.rb
index dfb74ae2048873521ce675e1996481df23860aef..e217474f7f472dd9f27fc96541d3ceecc6b36f63 100644
--- a/ee/spec/models/namespaces/free_user_cap/enforcement_spec.rb
+++ b/ee/spec/models/namespaces/free_user_cap/enforcement_spec.rb
@@ -484,6 +484,77 @@
     end
   end
 
+  describe '#at_limit?' do
+    let(:free_plan_members_count) { Namespaces::FreeUserCap.dashboard_limit + 1 }
+
+    subject(:at_limit?) { described_class.new(namespace).at_limit? }
+
+    before do
+      allow(::Namespaces::FreeUserCap::UsersFinder).to receive(:count).and_return(free_plan_members_count)
+    end
+
+    context 'when :free_user_cap is disabled' do
+      before do
+        stub_feature_flags(free_user_cap: false)
+      end
+
+      it { is_expected.to be false }
+    end
+
+    context 'when :free_user_cap is enabled' do
+      let(:free_plan_members_count) { Namespaces::FreeUserCap.dashboard_limit }
+
+      it { is_expected.to be false }
+
+      context 'with a net new namespace' do
+        include_context 'with net new namespace'
+
+        context 'when enforcement date is populated' do
+          before do
+            stub_ee_application_setting(dashboard_limit_new_namespace_creation_enforcement_date: enforcement_date)
+          end
+
+          context 'when :free_user_cap_new_namespaces is enabled' do
+            before do
+              stub_ee_application_setting(dashboard_limit: 3)
+              stub_feature_flags(free_user_cap_new_namespaces: true)
+            end
+
+            context 'when under the dashboard_limit' do
+              let(:free_plan_members_count) { 2 }
+
+              it { is_expected.to be false }
+            end
+
+            context 'when at the dashboard_limit' do
+              let(:free_plan_members_count) { 3 }
+
+              it { is_expected.to be true }
+            end
+
+            context 'when over the dashboard_limit' do
+              let(:free_plan_members_count) { 4 }
+
+              it { is_expected.to be false }
+            end
+          end
+
+          context 'when :free_user_cap_new_namespaces is disabled it honors existing namespace logic' do
+            before do
+              stub_feature_flags(free_user_cap_new_namespaces: false)
+            end
+
+            it { is_expected.to be false }
+          end
+        end
+
+        context 'when enforcement date is not populated it honors existing namespace logic' do
+          it { is_expected.to be false }
+        end
+      end
+    end
+  end
+
   describe '#seat_available?' do
     let(:free_plan_members_count) { Namespaces::FreeUserCap.dashboard_limit + 1 }
     let_it_be(:user) { create(:user) }
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index def7f2d018aff29d62a886dfa4c1ecf6721aaff9..a0cfd270916892d38805ef48d6875f2578d3265e 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -43034,6 +43034,11 @@ msgstr ""
 msgid "To import an SVN repository, check out %{svn_link}."
 msgstr ""
 
+msgid "To invite more users, you can reduce the number of users in your namespace to %{free_limit} user or less. You can also upgrade to a paid tier which do not have user limits. If you need additional time, you can start a free 30-day trial which includes unlimited users."
+msgid_plural "To invite more users, you can reduce the number of users in your namespace to %{free_limit} users or less. You can also upgrade to a paid tier which do not have user limits. If you need additional time, you can start a free 30-day trial which includes unlimited users."
+msgstr[0] ""
+msgstr[1] ""
+
 msgid "To keep this project going, create a new issue"
 msgstr ""
 
@@ -47997,6 +48002,9 @@ msgstr ""
 msgid "Your name"
 msgstr ""
 
+msgid "Your namespace %{namespace_name} has reached the %{free_limit} user limit"
+msgstr ""
+
 msgid "Your namespace %{namespace_name} is over the %{free_limit} user limit and has been placed in a read-only state."
 msgstr ""