diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 1affdd8f4339b6bf6ea4eee597885e8cb0e78a61..2af212fd75c80b0d1451971907a99796d06e993a 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -514,7 +514,8 @@ def visible_attributes
       :ci_max_total_yaml_size_bytes,
       :project_jobs_api_rate_limit,
       :security_txt_content,
-      :allow_project_creation_for_guest_and_below
+      :allow_project_creation_for_guest_and_below,
+      :downstream_pipeline_trigger_limit_per_project_user_sha
     ].tap do |settings|
       next if Gitlab.com?
 
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 4cb918c9c8b0dbec42cf5558022f5862f4e791d5..e39dc127641499a8f6e22ce805e1211280b631af 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -597,11 +597,13 @@ def self.kroki_formats_attributes
       :sidekiq_job_limiter_compression_threshold_bytes,
       :sidekiq_job_limiter_limit_bytes,
       :terminal_max_session_time,
-      :users_get_by_id_limit
+      :users_get_by_id_limit,
+      :downstream_pipeline_trigger_limit_per_project_user_sha
   end
 
   jsonb_accessor :rate_limits,
-    members_delete_limit: [:integer, { default: 60 }]
+    members_delete_limit: [:integer, { default: 60 }],
+    downstream_pipeline_trigger_limit_per_project_user_sha: [:integer, { default: 0 }]
 
   validates :rate_limits, json_schema: { filename: "application_setting_rate_limits" }
 
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index e0b334780371e4f12c78eec3b60add17fac50afd..c3ff9c50e764661c36810d1e695d93148b121a70 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -280,7 +280,8 @@ def defaults # rubocop:disable Metrics/AbcSize
         security_txt_content: nil,
         allow_project_creation_for_guest_and_below: true,
         enable_member_promotion_management: false,
-        security_approval_policies_limit: 5
+        security_approval_policies_limit: 5,
+        downstream_pipeline_trigger_limit_per_project_user_sha: 0
       }.tap do |hsh|
         hsh.merge!(non_production_defaults) unless Rails.env.production?
       end
diff --git a/app/services/ci/trigger_downstream_pipeline_service.rb b/app/services/ci/trigger_downstream_pipeline_service.rb
index ac95c4596b1379a7cb82ebf80639ca37d4503cf0..4fc09fd749288250a98972c1813d3aee6d113646 100644
--- a/app/services/ci/trigger_downstream_pipeline_service.rb
+++ b/app/services/ci/trigger_downstream_pipeline_service.rb
@@ -3,10 +3,6 @@
 module Ci
   # Enqueues the downstream pipeline worker.
   class TriggerDownstreamPipelineService
-    # This is a temporary constant. It may be converted into an application setting
-    # in the future. See https://gitlab.com/gitlab-org/gitlab/-/issues/425941.
-    DOWNSTREAM_PIPELINE_TRIGGER_LIMIT_PER_PROJECT_USER_SHA = 200
-
     def initialize(bridge)
       @bridge = bridge
       @current_user = bridge.user
diff --git a/app/validators/json_schemas/application_setting_rate_limits.json b/app/validators/json_schemas/application_setting_rate_limits.json
index e74295291dfdc0eb7f1e21dc75111686096331fa..183c6327447175215883caa1477cebcd9f3f5753 100644
--- a/app/validators/json_schemas/application_setting_rate_limits.json
+++ b/app/validators/json_schemas/application_setting_rate_limits.json
@@ -8,6 +8,11 @@
       "type": "integer",
       "minimum": 0,
       "description": "Number of project or group members a user can delete per minute."
+    },
+    "downstream_pipeline_trigger_limit_per_project_user_sha": {
+      "type": "integer",
+      "minimum": 0,
+      "description": "Maximum number of downstream pipelines triggered in a project per user"
     }
   }
 }
diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml
index f63a9862c13c5e05effd4382d1cf58895dace537..a808c580c2fe84c49110feabd51bf9446098fc9d 100644
--- a/app/views/admin/application_settings/_ci_cd.html.haml
+++ b/app/views/admin/application_settings/_ci_cd.html.haml
@@ -50,6 +50,11 @@
         = f.number_field :ci_max_includes, class: 'form-control gl-form-input'
         .form-text.text-muted
           = s_('AdminSettings|The maximum number of included files per pipeline.')
+      .form-group
+        = f.label :downstream_pipeline_trigger_limit_per_project_user_sha, s_('AdminSettings|Maximum downstream pipelines triggered in a project per user'), class: 'label-bold'
+        = f.number_field :downstream_pipeline_trigger_limit_per_project_user_sha, min: 0, class: 'form-control gl-form-input'
+        .form-text.text-muted
+          = s_('AdminSettings|The maximum number of downstream pipelines triggered in a project per user.')
       .form-group
         = f.label :ci_config_path, _('Default CI/CD configuration file'), class: 'label-bold'
         = f.text_field :default_ci_config_path, class: 'form-control gl-form-input', placeholder: '.gitlab-ci.yml'
diff --git a/doc/administration/settings/continuous_integration.md b/doc/administration/settings/continuous_integration.md
index 67f5889dd61ecd086a4ad056b536c4d73883a84a..d8ba43d78a29d0411dae2bf97c67049881bd4b24 100644
--- a/doc/administration/settings/continuous_integration.md
+++ b/doc/administration/settings/continuous_integration.md
@@ -205,6 +205,18 @@ The default is `150`.
 1. Change the value of **Maximum includes**.
 1. Select **Save changes** for the changes to take effect.
 
+## Maximum downstream pipelines triggered per project
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/144077) in GitLab 16.10 [with feature flag](../feature_flags.md) named `ci_rate_limit_downstream_pipelines`. Disabled by default.
+
+The maximum number of [downstream pipelines](../../ci/pipelines/downstream_pipelines.md) per project per user can be set at the instance level.
+The default is `0` (no restriction).
+
+1. On the left sidebar, at the bottom, select **Admin Area**.
+1. Select **Settings > CI/CD**.
+1. Change the value of **Maximum downstream pipelines triggered in a project per user**.
+1. Select **Save changes** for the changes to take effect.
+
 ## Default CI/CD configuration file
 
 > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/18073) in GitLab 12.5.
diff --git a/doc/api/settings.md b/doc/api/settings.md
index 5e2663e9fec071d8466ed4a3c880e78159fb0a4a..922cb2bd3c86d679ae175985665e91635ef15758 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -281,7 +281,8 @@ Example response:
   "bulk_import_max_download_file_size": 5120,
   "project_jobs_api_rate_limit": 600,
   "security_txt_content": null,
-  "bulk_import_concurrent_pipeline_batch_limit": 25
+  "bulk_import_concurrent_pipeline_batch_limit": 25,
+  "downstream_pipeline_trigger_limit_per_project_user_sha": 0
 }
 ```
 
@@ -390,6 +391,7 @@ listed in the descriptions of the relevant settings.
 | `domain_denylist_enabled`                | boolean          | no                                   | (**If enabled, requires:** `domain_denylist`) Allows blocking sign-ups from emails from specific domains. |
 | `domain_denylist`                        | array of strings | no                                   | Users with email addresses that match these domains **cannot** sign up. Wildcards allowed. Use separate lines for multiple entries. For example: `domain.com`, `*.domain.com`. |
 | `domain_allowlist`                       | array of strings | no                                   | Force people to use only corporate emails for sign-up. Default is `null`, meaning there is no restriction. |
+| `downstream_pipeline_trigger_limit_per_project_user_sha` | integer | no                            | Rate limit creation of downstream pipelines. Default: `0`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/144077) in GitLab 16.10 with a [flag](../administration/feature_flags.md) named `ci_rate_limit_downstream_pipelines`. Disabled by default. |
 | `dsa_key_restriction`                    | integer          | no                                   | The minimum allowed bit length of an uploaded DSA key. Default is `0` (no restriction). `-1` disables DSA keys. |
 | `ecdsa_key_restriction`                  | integer          | no                                   | The minimum allowed curve size (in bits) of an uploaded ECDSA key. Default is `0` (no restriction). `-1` disables ECDSA keys. |
 | `ecdsa_sk_key_restriction`               | integer          | no                                   | The minimum allowed curve size (in bits) of an uploaded ECDSA_SK key. Default is `0` (no restriction). `-1` disables ECDSA_SK keys. |
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index 6ef68ccc3de6383cd160a0eab4f91f30a93c08b3..8c542db06a74c1497850f64eee20f0ecad3190a8 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -229,6 +229,7 @@ def filter_attributes_using_license(attrs)
       optional :namespace_aggregation_schedule_lease_duration_in_seconds, type: Integer, desc: 'Maximum duration (in seconds) between refreshes of namespace statistics (Default: 300)'
       optional :project_jobs_api_rate_limit, type: Integer, desc: 'Maximum authenticated requests to /project/:id/jobs per minute'
       optional :security_txt_content, type: String, desc: 'Public security contact information made available at https://gitlab.example.com/.well-known/security.txt'
+      optional :downstream_pipeline_trigger_limit_per_project_user_sha, type: Integer, desc: 'Maximum number of downstream pipelines triggered per project'
 
       Gitlab::SSHPublicKey.supported_types.each do |type|
         optional :"#{type}_key_restriction",
diff --git a/lib/gitlab/application_rate_limiter.rb b/lib/gitlab/application_rate_limiter.rb
index 2e992e38a44c70f9896174f2415f400ee6215f31..e13b54e40eea40bfaaae9062a6156d52cd7feb41 100644
--- a/lib/gitlab/application_rate_limiter.rb
+++ b/lib/gitlab/application_rate_limiter.rb
@@ -67,7 +67,7 @@ def rate_limits # rubocop:disable Metrics/AbcSize
             threshold: -> { application_settings.projects_api_rate_limit_unauthenticated }, interval: 10.minutes
           },
           downstream_pipeline_trigger: {
-            threshold: -> { ::Ci::TriggerDownstreamPipelineService::DOWNSTREAM_PIPELINE_TRIGGER_LIMIT_PER_PROJECT_USER_SHA }, interval: 1.minute
+            threshold: -> { application_settings.downstream_pipeline_trigger_limit_per_project_user_sha }, interval: 1.minute
           }
         }.freeze
       end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 65bb8ec785da09ba5b52e9127a4a637c13146203..5b49c71adee3c3a6066901a2cca67d76b2c870b3 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -3584,6 +3584,9 @@ msgstr ""
 msgid "AdminSettings|Limit the number of namespaces and projects that can be indexed."
 msgstr ""
 
+msgid "AdminSettings|Maximum downstream pipelines triggered in a project per user"
+msgstr ""
+
 msgid "AdminSettings|Maximum duration of a session for Git operations when 2FA is enabled."
 msgstr ""
 
@@ -3743,6 +3746,9 @@ msgstr ""
 msgid "AdminSettings|The latest artifacts for all jobs in the most recent successful pipelines in each project are stored and do not expire."
 msgstr ""
 
+msgid "AdminSettings|The maximum number of downstream pipelines triggered in a project per user."
+msgstr ""
+
 msgid "AdminSettings|The maximum number of included files per pipeline."
 msgstr ""
 
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index d1fdbfc5329f33e7788d5a04bc9d0bd334b25911..8db9c45dbef90f98b06fc9bfb349564e4b874018 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -459,6 +459,7 @@
           uncheck 'Keep the latest artifacts for all jobs in the latest successful pipelines'
           uncheck 'Enable pipeline suggestion banner'
           fill_in 'application_setting_ci_max_includes', with: 200
+          fill_in 'application_setting_downstream_pipeline_trigger_limit_per_project_user_sha', with: 500
           click_button 'Save changes'
         end
 
@@ -467,6 +468,7 @@
         expect(current_settings.keep_latest_artifact).to be false
         expect(current_settings.suggest_pipeline_enabled).to be false
         expect(current_settings.ci_max_includes).to be 200
+        expect(current_settings.downstream_pipeline_trigger_limit_per_project_user_sha).to be 500
         expect(page).to have_content "Application settings saved successfully"
       end
 
diff --git a/spec/helpers/application_settings_helper_spec.rb b/spec/helpers/application_settings_helper_spec.rb
index b378437c407555a70216ecb84af2570a4c2fd74f..43075a75d1ca1f025626be5bb0fa958f6fb52735 100644
--- a/spec/helpers/application_settings_helper_spec.rb
+++ b/spec/helpers/application_settings_helper_spec.rb
@@ -65,7 +65,7 @@
           project_download_export_limit project_export_limit project_import_limit
           raw_blob_request_limit group_export_limit group_download_export_limit
           group_import_limit users_get_by_id_limit search_rate_limit search_rate_limit_unauthenticated
-          members_delete_limit
+          members_delete_limit downstream_pipeline_trigger_limit_per_project_user_sha
         ])
     end
 
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 18060d29ad52c74a383d5a7c13e8fdd6cb9f4c61..d88415527ad420f5634d95a5d8b0856bea419416 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -29,6 +29,7 @@
     it { expect(setting.bulk_import_concurrent_pipeline_batch_limit).to eq(25) }
     it { expect(setting.allow_project_creation_for_guest_and_below).to eq(true) }
     it { expect(setting.members_delete_limit).to eq(60) }
+    it { expect(setting.downstream_pipeline_trigger_limit_per_project_user_sha).to eq(0) }
   end
 
   describe 'validations' do
@@ -244,6 +245,7 @@ def many_usernames(num = 100)
           sidekiq_job_limiter_limit_bytes
           terminal_max_session_time
           users_get_by_id_limit
+          downstream_pipeline_trigger_limit_per_project_user_sha
         ]
       end
 
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 01aeb88a1096859b4ac2119149d6ff2ceeafbc5e..018e6b9786418ba01d91b9c8c76a107322337ddc 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -95,6 +95,7 @@
       expect(json_response['max_login_attempts']).to be_nil
       expect(json_response['failed_login_attempts_unlock_period_in_minutes']).to be_nil
       expect(json_response['bulk_import_concurrent_pipeline_batch_limit']).to eq(25)
+      expect(json_response['downstream_pipeline_trigger_limit_per_project_user_sha']).to eq(0)
     end
   end
 
@@ -216,7 +217,8 @@
             gitlab_shell_operation_limit: 500,
             namespace_aggregation_schedule_lease_duration_in_seconds: 400,
             max_import_remote_file_size: 2,
-            security_txt_content: nil
+            security_txt_content: nil,
+            downstream_pipeline_trigger_limit_per_project_user_sha: 300
           }
 
         expect(response).to have_gitlab_http_status(:ok)
@@ -302,6 +304,7 @@
         expect(json_response['bulk_import_max_download_file_size']).to be(1)
         expect(json_response['security_txt_content']).to be(nil)
         expect(json_response['bulk_import_concurrent_pipeline_batch_limit']).to be(2)
+        expect(json_response['downstream_pipeline_trigger_limit_per_project_user_sha']).to be(300)
       end
     end
 
@@ -1023,6 +1026,40 @@
       end
     end
 
+    context 'with downstream_pipeline_trigger_limit_per_project_user_sha' do
+      it 'updates the settings' do
+        put api("/application/settings", admin), params: {
+          downstream_pipeline_trigger_limit_per_project_user_sha: 200
+        }
+
+        expect(response).to have_gitlab_http_status(:ok)
+        expect(json_response).to include(
+          'downstream_pipeline_trigger_limit_per_project_user_sha' => 200
+        )
+      end
+
+      it 'allows a zero value' do
+        put api("/application/settings", admin), params: {
+          downstream_pipeline_trigger_limit_per_project_user_sha: 0
+        }
+
+        expect(response).to have_gitlab_http_status(:ok)
+        expect(json_response).to include(
+          'downstream_pipeline_trigger_limit_per_project_user_sha' => 0
+        )
+      end
+
+      it 'does not allow a nil value' do
+        put api("/application/settings", admin), params: {
+          downstream_pipeline_trigger_limit_per_project_user_sha: nil
+        }
+
+        expect(response).to have_gitlab_http_status(:bad_request)
+        expect(json_response['message']['downstream_pipeline_trigger_limit_per_project_user_sha'])
+          .to include(a_string_matching('is not a number'))
+      end
+    end
+
     context 'with housekeeping enabled' do
       it 'at least one of housekeeping_incremental_repack_period or housekeeping_optimize_repository_period is required' do
         put api("/application/settings", admin), params: {
diff --git a/spec/services/ci/trigger_downstream_pipeline_service_spec.rb b/spec/services/ci/trigger_downstream_pipeline_service_spec.rb
index 71d6931658925faa279856864c9bdf5c9edb03d9..299f765b95cd8cb69254e0386cf98fb8254a33c7 100644
--- a/spec/services/ci/trigger_downstream_pipeline_service_spec.rb
+++ b/spec/services/ci/trigger_downstream_pipeline_service_spec.rb
@@ -50,7 +50,7 @@
 
       context 'when the limit is exceeded' do
         before do
-          stub_const("#{described_class.name}::DOWNSTREAM_PIPELINE_TRIGGER_LIMIT_PER_PROJECT_USER_SHA", 1)
+          stub_application_setting(downstream_pipeline_trigger_limit_per_project_user_sha: 1)
         end
 
         it 'drops the bridge and does not schedule the downstream pipeline worker', :aggregate_failures do