diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 42c9481828c16055b21d6879034f8fad7df18b4f..a7694c5858856729a286c9f1d578e5fc377f6570 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -484,7 +484,8 @@ def visible_attributes
       :user_defaults_to_private_profile,
       :deactivation_email_additional_text,
       :projects_api_rate_limit_unauthenticated,
-      :gitlab_dedicated_instance
+      :gitlab_dedicated_instance,
+      :ci_max_includes
     ].tap do |settings|
       next if Gitlab.com?
 
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 02b8cf9ee28b9cb0127f8e065da3a4aed1fd7f3b..6a34a7dd14845dcf9e3cf736620c5cbbe03c3512 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -383,6 +383,8 @@ def self.kroki_formats_attributes
   validates :max_yaml_size_bytes, numericality: { only_integer: true, greater_than: 0 }, presence: true
   validates :max_yaml_depth, numericality: { only_integer: true, greater_than: 0 }, presence: true
 
+  validates :ci_max_includes, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, presence: true
+
   validates :email_restrictions, untrusted_regexp: true
 
   validates :hashed_storage_enabled, inclusion: { in: [true], message: N_("Hashed storage can't be disabled anymore for new projects") }
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 010c88179df0424fa38d8f8a4aba9c95acd18a3a..845d402f550474e0ab3f9bd6e6a7fe97e75afa08 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -252,7 +252,8 @@ def defaults # rubocop:disable Metrics/AbcSize
         allow_runner_registration_token: true,
         user_defaults_to_private_profile: false,
         projects_api_rate_limit_unauthenticated: 400,
-        gitlab_dedicated_instance: false
+        gitlab_dedicated_instance: false,
+        ci_max_includes: 150
       }.tap do |hsh|
         hsh.merge!(non_production_defaults) unless Rails.env.production?
       end
diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml
index f162f5be74644286a0e181b20c34c48cb7b54fdf..0c9d5a5a8dff5ffa3b7ec133091975910b2ca433 100644
--- a/app/views/admin/application_settings/_ci_cd.html.haml
+++ b/app/views/admin/application_settings/_ci_cd.html.haml
@@ -45,6 +45,11 @@
           = link_to _('Learn more.'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'archive-jobs')
       .form-group
         = f.gitlab_ui_checkbox_component :protected_ci_variables, s_('AdminSettings|Protect CI/CD variables by default'), help_text: s_('AdminSettings|New CI/CD variables in projects and groups default to protected.')
+      .form-group
+        = f.label :ci_max_includes, s_('AdminSettings|Maximum includes'), class: 'label-bold'
+        = 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 :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/db/migrate/20230421165020_add_ci_max_includes_to_application_settings.rb b/db/migrate/20230421165020_add_ci_max_includes_to_application_settings.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9996d3b1654d2087d17a784a51b0e33e3745dcb1
--- /dev/null
+++ b/db/migrate/20230421165020_add_ci_max_includes_to_application_settings.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddCiMaxIncludesToApplicationSettings < Gitlab::Database::Migration[2.1]
+  def change
+    add_column :application_settings, :ci_max_includes, :integer, default: 150, null: false
+  end
+end
diff --git a/db/schema_migrations/20230421165020 b/db/schema_migrations/20230421165020
new file mode 100644
index 0000000000000000000000000000000000000000..a1121e3f83df1b39464ba1a8b8a780531838b838
--- /dev/null
+++ b/db/schema_migrations/20230421165020
@@ -0,0 +1 @@
+1e33e8da1f2f10f976e19bcf234f7ac5bdcc0eed7d60288ead2eaf87dfd678c9
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index c366c1e3bb64e1f57cd88f3a4088b365b737ef75..b9ea2dcbaa3d1d8fa07b84d156346a21ca262c42 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -11804,6 +11804,7 @@ CREATE TABLE application_settings (
     encrypted_product_analytics_configurator_connection_string_iv bytea,
     silent_mode_enabled boolean DEFAULT false NOT NULL,
     package_metadata_purl_types smallint[] DEFAULT '{}'::smallint[],
+    ci_max_includes integer DEFAULT 150 NOT NULL,
     CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)),
     CONSTRAINT app_settings_container_registry_pre_import_tags_rate_positive CHECK ((container_registry_pre_import_tags_rate >= (0)::numeric)),
     CONSTRAINT app_settings_dep_proxy_ttl_policies_worker_capacity_positive CHECK ((dependency_proxy_ttl_group_policy_worker_capacity >= 0)),
diff --git a/doc/api/settings.md b/doc/api/settings.md
index 22e21ba19b4cdc0dd671bfbebee0b7c56ae2c111..9c8c62ed2230d58bfee7f4e7688dfaec99e3cbfd 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -296,6 +296,7 @@ listed in the descriptions of the relevant settings.
 | `bulk_import_enabled`                    | boolean          | no                                   | Enable migrating GitLab groups by direct transfer. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/383268) in GitLab 15.8. Setting also [available](../user/admin_area/settings/visibility_and_access_controls.md#enable-migration-of-groups-and-projects-by-direct-transfer) in the Admin Area. |
 | `can_create_group` | boolean | no | Indicates whether users can create top-level groups. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/367754) in GitLab 15.5. Defaults to `true`. |
 | `check_namespace_plan` **(PREMIUM)**     | boolean          | no                                   | Enabling this makes only licensed EE features available to projects if the project namespace's plan includes the feature or if the project is public. |
+| `ci_max_includes`                        | integer          | no                                   | The maximum number of [includes](../ci/yaml/includes.md) per pipeline. Default is `150`. |
 | `commit_email_hostname`                  | string           | no                                   | Custom hostname (for private commit emails). |
 | `container_expiration_policies_enable_historic_entries`  | boolean | no                            | Enable [cleanup policies](../user/packages/container_registry/reduce_container_registry_storage.md#enable-the-cleanup-policy) for all projects. |
 | `container_registry_cleanup_tags_service_max_list_size`  | integer | no                            | The maximum number of tags that can be deleted in a single execution of [cleanup policies](../user/packages/container_registry/reduce_container_registry_storage.md#set-cleanup-limits-to-conserve-resources). |
diff --git a/doc/ci/yaml/includes.md b/doc/ci/yaml/includes.md
index cd2cb3c96e1d92f1c264286386bd29a4b4ca2d99..35b302d03734d4554e4ff2f917f54e0c18a60634 100644
--- a/doc/ci/yaml/includes.md
+++ b/doc/ci/yaml/includes.md
@@ -603,3 +603,6 @@ To help reduce the risk of this happening, edit the pipeline configuration file
 with the [pipeline editor](../pipeline_editor/index.md), which validates if the
 limit is reached. You can remove one included file at a time to try to narrow down
 which configuration file is the source of the loop or excessive included files.
+
+In [GitLab 16.0 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/207270) self-managed users can
+change the [maximum includes](../../user/admin_area/settings/continuous_integration.md#maximum-includes) value.
diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md
index 5090e016cb01b4bafd48846c5a8fe6d7f3e456da..39f979d98d5828e60a2fada11df9938d885a87c0 100644
--- a/doc/ci/yaml/index.md
+++ b/doc/ci/yaml/index.md
@@ -136,16 +136,6 @@ The `include` files are:
 - Always evaluated first and then merged with the content of the `.gitlab-ci.yml` file,
   regardless of the position of the `include` keyword.
 
-You can have up to 150 includes per pipeline, including [nested](includes.md#use-nested-includes) includes:
-
-- In [GitLab 15.10 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/367150) you can have up to 150 includes.
-  In nested includes, the same file can be included multiple times, but duplicated includes
-  count towards the limit.
-- From [GitLab 14.9 to GitLab 15.9](https://gitlab.com/gitlab-org/gitlab/-/issues/28987), you can have up to 100 includes.
-  The same file can be included multiple times in nested includes, but duplicates are ignored.
-- In GitLab 14.9 and earlier you can have up to 100 includes, but the same file can not
-  be included multiple times.
-
 The time limit to resolve all files is 30 seconds.
 
 **Keyword type**: Global keyword.
@@ -171,6 +161,16 @@ The time limit to resolve all files is 30 seconds.
     do not affect job reruns.
   - Pipeline, the `include` files are fetched again. If they changed after the last
     pipeline run, the new pipeline uses the changed configuration.
+- You can have up to 150 includes per pipeline by default, including [nested](includes.md#use-nested-includes). Additionally:
+  - In [GitLab 16.0 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/207270) self-managed users can
+    change the [maximum includes](../../user/admin_area/settings/continuous_integration.md#maximum-includes) value.
+  - In [GitLab 15.10 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/367150) you can have up to 150 includes.
+    In nested includes, the same file can be included multiple times, but duplicated includes
+    count towards the limit.
+  - From [GitLab 14.9 to GitLab 15.9](https://gitlab.com/gitlab-org/gitlab/-/issues/28987), you can have up to 100 includes.
+    The same file can be included multiple times in nested includes, but duplicates are ignored.
+  - In GitLab 14.9 and earlier you can have up to 100 includes, but the same file can not
+    be included multiple times.
 
 **Related topics**:
 
diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md
index a0a0dc6509972732d9978df652de5a4cb96b3c81..d6b1d243d823b0e24f19050007eef2ba480bf5ec 100644
--- a/doc/user/admin_area/settings/continuous_integration.md
+++ b/doc/user/admin_area/settings/continuous_integration.md
@@ -194,6 +194,16 @@ To set all new [CI/CD variables](../../../ci/variables/index.md) as
 1. On the left sidebar, select **Settings > CI/CD**.
 1. Select **Protect CI/CD variables by default**.
 
+## Maximum includes
+
+The maximum number of [includes](../../../ci/yaml/includes.md) per pipeline can be set at the instance level.
+The default is `150`.
+
+1. On the top bar, select **Main menu > Admin**.
+1. On the left sidebar, select **Settings > CI/CD**.
+1. Change the value of **Maximum includes**.
+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/lib/api/settings.rb b/lib/api/settings.rb
index 9562af225a7efacee1e619125a2e2612850118ed..f3204104b9f41c5ea1c9b48206ccd720e001fdfd 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -196,6 +196,7 @@ def filter_attributes_using_license(attrs)
       optional :jira_connect_proxy_url, type: String, desc: "URL of the GitLab instance that should be used as a proxy for the GitLab for Jira Cloud app"
       optional :bulk_import_enabled, type: Boolean, desc: 'Enable migrating GitLab groups and projects by direct transfer'
       optional :allow_runner_registration_token, type: Boolean, desc: 'Allow registering runners using a registration token'
+      optional :ci_max_includes, type: Integer, desc: 'Maximum number of includes per pipeline'
 
       Gitlab::SSHPublicKey.supported_types.each do |type|
         optional :"#{type}_key_restriction",
diff --git a/lib/gitlab/ci/config/external/context.rb b/lib/gitlab/ci/config/external/context.rb
index 156109a084d69bd9f0f575473536e2de1c084182..b8e012ec8515ceac8221cffcf86ca77766c87cee 100644
--- a/lib/gitlab/ci/config/external/context.rb
+++ b/lib/gitlab/ci/config/external/context.rb
@@ -9,8 +9,7 @@ class Context
 
           TimeoutError = Class.new(StandardError)
 
-          MAX_INCLUDES = 150
-          TEMP_MAX_INCLUDES = 100 # For logging; to be removed in https://gitlab.com/gitlab-org/gitlab/-/issues/367150
+          TEMP_MAX_INCLUDES = 100 # For logging; to be removed in https://gitlab.com/gitlab-org/gitlab/-/issues/396776
 
           include ::Gitlab::Utils::StrongMemoize
 
@@ -32,7 +31,7 @@ def initialize(
             @expandset = []
             @execution_deadline = 0
             @logger = logger || Gitlab::Ci::Pipeline::Logger.new(project: project)
-            @max_includes = MAX_INCLUDES
+            @max_includes = Gitlab::CurrentSettings.current_application_settings.ci_max_includes
             yield self if block_given?
           end
 
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index e6b93cf41ac2e42ab77725cd08b6034297d1b1ad..66003ce06512f960f30b2b901a99302a4218b7e1 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -3075,6 +3075,9 @@ msgstr ""
 msgid "AdminSettings|Maximum duration of a session for Git operations when 2FA is enabled."
 msgstr ""
 
+msgid "AdminSettings|Maximum includes"
+msgstr ""
+
 msgid "AdminSettings|Maximum number of DAG dependencies that a job can have"
 msgstr ""
 
@@ -3228,6 +3231,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 included files per pipeline."
+msgstr ""
+
 msgid "AdminSettings|The template for the required pipeline configuration can be one of the GitLab-provided templates, or a custom template added to an instance template repository. %{link_start}How do I create an instance template repository?%{link_end}"
 msgstr ""
 
diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb
index 253f66579f3c6283fe14af75f41eeeb4b86d4c27..4b6274c2d2631392041a8957ab96a2b531db5f5c 100644
--- a/spec/controllers/admin/application_settings_controller_spec.rb
+++ b/spec/controllers/admin/application_settings_controller_spec.rb
@@ -2,7 +2,7 @@
 
 require 'spec_helper'
 
-RSpec.describe Admin::ApplicationSettingsController, :do_not_mock_admin_mode_setting do
+RSpec.describe Admin::ApplicationSettingsController, :do_not_mock_admin_mode_setting, feature_category: :shared do
   include StubENV
   include UsageDataHelpers
 
@@ -398,6 +398,17 @@
         expect(application_settings.reload.invitation_flow_enforcement).to eq(true)
       end
     end
+
+    context 'maximum includes' do
+      let(:application_settings) { ApplicationSetting.current }
+
+      it 'updates ci_max_includes setting' do
+        put :update, params: { application_setting: { ci_max_includes: 200 } }
+
+        expect(response).to redirect_to(general_admin_application_settings_path)
+        expect(application_settings.reload.ci_max_includes).to eq(200)
+      end
+    end
   end
 
   describe 'PUT #reset_registration_token', feature_category: :user_management do
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index 524fcb01644eec342b0c7bb2325e56a7c2a82747..4dee97880a5a553bba3ceaa6bf91dc88e23a5d45 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -426,6 +426,7 @@
           fill_in 'application_setting_auto_devops_domain', with: 'domain.com'
           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
           click_button 'Save changes'
         end
 
@@ -433,6 +434,7 @@
         expect(current_settings.auto_devops_domain).to eq('domain.com')
         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(page).to have_content "Application settings saved successfully"
       end
 
diff --git a/spec/lib/gitlab/ci/config/external/context_spec.rb b/spec/lib/gitlab/ci/config/external/context_spec.rb
index fc68d3d62a25f584650e10e7e36472416723d15c..d917924f2577932875e611cad86072a45d1d22cf 100644
--- a/spec/lib/gitlab/ci/config/external/context_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/context_spec.rb
@@ -24,7 +24,6 @@
     context 'with values' do
       it { is_expected.to have_attributes(**attributes) }
       it { expect(subject.expandset).to eq([]) }
-      it { expect(subject.max_includes).to eq(Gitlab::Ci::Config::External::Context::MAX_INCLUDES) }
       it { expect(subject.execution_deadline).to eq(0) }
       it { expect(subject.variables).to be_instance_of(Gitlab::Ci::Variables::Collection) }
       it { expect(subject.variables_hash).to be_instance_of(ActiveSupport::HashWithIndifferentAccess) }
@@ -37,12 +36,27 @@
 
       it { is_expected.to have_attributes(**attributes) }
       it { expect(subject.expandset).to eq([]) }
-      it { expect(subject.max_includes).to eq(Gitlab::Ci::Config::External::Context::MAX_INCLUDES) }
       it { expect(subject.execution_deadline).to eq(0) }
       it { expect(subject.variables).to be_instance_of(Gitlab::Ci::Variables::Collection) }
       it { expect(subject.variables_hash).to be_instance_of(ActiveSupport::HashWithIndifferentAccess) }
       it { expect(subject.pipeline_config).to be_nil }
     end
+
+    describe 'max_includes' do
+      it 'returns the default value of application setting `ci_max_includes`' do
+        expect(subject.max_includes).to eq(150)
+      end
+
+      context 'when application setting `ci_max_includes` is changed' do
+        before do
+          stub_application_setting(ci_max_includes: 200)
+        end
+
+        it 'returns the new value of application setting `ci_max_includes`' do
+          expect(subject.max_includes).to eq(200)
+        end
+      end
+    end
   end
 
   describe '#set_deadline' do
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 64411c9fe928be0bc4139118024b90e2e9ba0c07..7b5c3c82b7f180f83f8bf57c56cdb48745f12f28 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -277,6 +277,13 @@ def many_usernames(num = 100)
     it { is_expected.to allow_value([true, false]).for(:silent_mode_enabled) }
     it { is_expected.not_to allow_value(nil).for(:silent_mode_enabled) }
 
+    it { is_expected.to allow_value(0).for(:ci_max_includes) }
+    it { is_expected.to allow_value(200).for(:ci_max_includes) }
+    it { is_expected.not_to allow_value('abc').for(:ci_max_includes) }
+    it { is_expected.not_to allow_value(nil).for(:ci_max_includes) }
+    it { is_expected.not_to allow_value(10.5).for(:ci_max_includes) }
+    it { is_expected.not_to allow_value(-1).for(:ci_max_includes) }
+
     context 'when deactivate_dormant_users is enabled' do
       before do
         stub_application_setting(deactivate_dormant_users: true)
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index ff86859a8f4a8abcf36ea1c23aabc959a4378ce2..8decb8cdf9035ec484481374f40e8a19722c2fe9 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -70,6 +70,7 @@
       expect(json_response['projects_api_rate_limit_unauthenticated']).to eq(400)
       expect(json_response['silent_mode_enabled']).to be(false)
       expect(json_response['valid_runner_registrars']).to match_array(%w(project group))
+      expect(json_response['ci_max_includes']).to eq(150)
     end
   end
 
@@ -819,6 +820,40 @@
       end
     end
 
+    context 'with ci_max_includes' do
+      it 'updates the settings' do
+        put api("/application/settings", admin), params: {
+          ci_max_includes: 200
+        }
+
+        expect(response).to have_gitlab_http_status(:ok)
+        expect(json_response).to include(
+          'ci_max_includes' => 200
+        )
+      end
+
+      it 'allows a zero value' do
+        put api("/application/settings", admin), params: {
+          ci_max_includes: 0
+        }
+
+        expect(response).to have_gitlab_http_status(:ok)
+        expect(json_response).to include(
+          'ci_max_includes' => 0
+        )
+      end
+
+      it 'does not allow a nil value' do
+        put api("/application/settings", admin), params: {
+          ci_max_includes: nil
+        }
+
+        expect(response).to have_gitlab_http_status(:bad_request)
+        expect(json_response['message']['ci_max_includes'])
+          .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: {