diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 60a229ef42aec5354cf003d2ab98a0da359266f0..177adb36cd44a341765885eecf19336b5bf44098 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -411,6 +411,9 @@ def visible_attributes
       :throttle_unauthenticated_files_api_enabled,
       :throttle_unauthenticated_files_api_period_in_seconds,
       :throttle_unauthenticated_files_api_requests_per_period,
+      :throttle_unauthenticated_git_http_enabled,
+      :throttle_unauthenticated_git_http_period_in_seconds,
+      :throttle_unauthenticated_git_http_requests_per_period,
       :throttle_unauthenticated_deprecated_api_enabled,
       :throttle_unauthenticated_deprecated_api_period_in_seconds,
       :throttle_unauthenticated_deprecated_api_requests_per_period,
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 959000af9ed86f2ba375bc544d83f8ce2cc05f47..415687c18b765d4aa5a9d5bdb0f082b93e6bd36e 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -554,6 +554,8 @@ def self.kroki_formats_attributes
       :throttle_unauthenticated_deprecated_api_requests_per_period,
       :throttle_unauthenticated_files_api_period_in_seconds,
       :throttle_unauthenticated_files_api_requests_per_period,
+      :throttle_unauthenticated_git_http_period_in_seconds,
+      :throttle_unauthenticated_git_http_requests_per_period,
       :throttle_unauthenticated_packages_api_period_in_seconds,
       :throttle_unauthenticated_packages_api_requests_per_period,
       :throttle_unauthenticated_period_in_seconds,
@@ -613,6 +615,11 @@ def self.kroki_formats_attributes
   jsonb_accessor :service_ping_settings,
     gitlab_environment_toolkit_instance: [:boolean, { default: false }]
 
+  jsonb_accessor :rate_limits_unauthenticated_git_http,
+    throttle_unauthenticated_git_http_enabled: [:boolean, { default: false }],
+    throttle_unauthenticated_git_http_requests_per_period: [:integer, { default: 3600 }],
+    throttle_unauthenticated_git_http_period_in_seconds: [:integer, { default: 3600 }]
+
   validates :rate_limits, json_schema: { filename: "application_setting_rate_limits" }
 
   validates :search_rate_limit_allowlist,
diff --git a/app/views/admin/application_settings/_git_http_limits.html.haml b/app/views/admin/application_settings/_git_http_limits.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..fc777a174431013f18ee43824874f4327fc4d10f
--- /dev/null
+++ b/app/views/admin/application_settings/_git_http_limits.html.haml
@@ -0,0 +1,18 @@
+= gitlab_ui_form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-git-http-limits-settings'), html: { class: 'fieldset-form' } do |f|
+  = form_errors(@application_setting)
+
+  %fieldset
+    %h5
+      = _('Unauthenticated Git HTTP request rate limit')
+    .form-group
+      = f.gitlab_ui_checkbox_component :throttle_unauthenticated_git_http_enabled,
+        _('Enable unauthenticated Git HTTP request rate limit'),
+        help_text: _('Helps reduce unauthenticated Git HTTP request volume for git paths.')
+    .form-group
+      = f.label :throttle_unauthenticated_git_http_requests_per_period, _('Maximum unauthenticated Git HTTP requests per period per IP'), class: 'gl-font-weight-bold'
+      = f.number_field :throttle_unauthenticated_git_http_requests_per_period, class: 'form-control gl-form-input'
+    .form-group
+      = f.label :throttle_unauthenticated_git_http_period_in_seconds, _('Unauthenticated Git HTTP rate limit period in seconds'), class: 'gl-font-weight-bold'
+      = f.number_field :throttle_unauthenticated_git_http_period_in_seconds, class: 'form-control gl-form-input'
+
+  = f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/network.html.haml b/app/views/admin/application_settings/network.html.haml
index 86fbbef8dac87e9d5ec5f6cb63ab16a6105b4e9b..b951c97814f7ec88de2afc8dcbf0ecbf1a1d46fa 100644
--- a/app/views/admin/application_settings/network.html.haml
+++ b/app/views/admin/application_settings/network.html.haml
@@ -72,6 +72,18 @@
   .settings-content
     = render partial: 'network_rate_limits', locals: { anchor: 'js-deprecated-limits-settings', setting_fragment: 'deprecated_api' }
 
+%section.settings.as-git-http-limits.no-animate#js-git-http-limits-settings{ class: ('expanded' if expanded_by_default?) }
+  .settings-header
+    %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
+      = _('Git HTTP rate limits')
+    = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
+      = expanded_by_default? ? _('Collapse') : _('Expand')
+    %p.gl-text-secondary
+      = _('Configure specific limits for Git HTTP requests.')
+      = link_to _('Learn more.'), help_page_path('administration/settings/git_http_rate_limits'), target: '_blank', rel: 'noopener noreferrer'
+  .settings-content
+    = render 'git_http_limits'
+
 %section.settings.as-git-lfs-limits.no-animate#js-git-lfs-limits-settings{ class: ('expanded' if expanded_by_default?) }
   .settings-header
     %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
diff --git a/db/migrate/20240405090000_add_throttle_unauthenticated_git_http_to_application_settings.rb b/db/migrate/20240405090000_add_throttle_unauthenticated_git_http_to_application_settings.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1881b8d630b856a142a65bf9d9a055484ece9cb4
--- /dev/null
+++ b/db/migrate/20240405090000_add_throttle_unauthenticated_git_http_to_application_settings.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class AddThrottleUnauthenticatedGitHttpToApplicationSettings < Gitlab::Database::Migration[2.2]
+  milestone '17.0'
+
+  disable_ddl_transaction!
+
+  def up
+    add_column :application_settings, :rate_limits_unauthenticated_git_http, :jsonb, default: {}, null: false,
+      if_not_exists: true
+
+    add_check_constraint(
+      :application_settings,
+      "(jsonb_typeof(rate_limits_unauthenticated_git_http) = 'object')",
+      'check_application_settings_rate_limits_unauth_git_http_is_hash'
+    )
+  end
+
+  def down
+    remove_column :application_settings, :rate_limits_unauthenticated_git_http, it_exists: true
+  end
+end
diff --git a/db/migrate/20240405090010_update_throttle_unauthenticated_git_http_in_application_settings.rb b/db/migrate/20240405090010_update_throttle_unauthenticated_git_http_in_application_settings.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d4458912262d09d644c67a477f3805047e66a5d1
--- /dev/null
+++ b/db/migrate/20240405090010_update_throttle_unauthenticated_git_http_in_application_settings.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class UpdateThrottleUnauthenticatedGitHttpInApplicationSettings < Gitlab::Database::Migration[2.2]
+  restrict_gitlab_migration gitlab_schema: :gitlab_main
+
+  milestone '17.0'
+
+  disable_ddl_transaction!
+
+  def up
+    execute <<~SQL
+      UPDATE application_settings
+      SET rate_limits_unauthenticated_git_http = jsonb_build_object(
+        'throttle_unauthenticated_git_http_enabled', throttle_unauthenticated_enabled,
+        'throttle_unauthenticated_git_http_requests_per_period', throttle_unauthenticated_requests_per_period,
+        'throttle_unauthenticated_git_http_period_in_seconds', throttle_unauthenticated_period_in_seconds
+      );
+    SQL
+  end
+
+  def down
+    execute "UPDATE application_settings SET rate_limits_unauthenticated_git_http = CAST('{}' AS jsonb)"
+  end
+end
diff --git a/db/schema_migrations/20240405090000 b/db/schema_migrations/20240405090000
new file mode 100644
index 0000000000000000000000000000000000000000..42e1ac97298bcc47f6317a723daf23f20c643ab9
--- /dev/null
+++ b/db/schema_migrations/20240405090000
@@ -0,0 +1 @@
+1f79208f10eac682970b91dd12159c9c7446fab637409d2f62dc91231af1dd13
\ No newline at end of file
diff --git a/db/schema_migrations/20240405090010 b/db/schema_migrations/20240405090010
new file mode 100644
index 0000000000000000000000000000000000000000..3b93267c8f9955c9cd9a9a54544db2c4ab10d6a2
--- /dev/null
+++ b/db/schema_migrations/20240405090010
@@ -0,0 +1 @@
+c84be5380fb2c1e90aabd987b8a7c020ec17405a8d0b97f0ca44e4ea724b7c6d
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 4a030710d3bac1723b03594673384024421e1663..53bf60f9e2b159f8350892299dcace955323903e 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -4319,6 +4319,7 @@ CREATE TABLE application_settings (
     include_optional_metrics_in_service_ping boolean DEFAULT true NOT NULL,
     zoekt_settings jsonb DEFAULT '{}'::jsonb NOT NULL,
     service_ping_settings jsonb DEFAULT '{}'::jsonb NOT NULL,
+    rate_limits_unauthenticated_git_http jsonb DEFAULT '{}'::jsonb 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)),
@@ -4371,6 +4372,7 @@ CREATE TABLE application_settings (
     CONSTRAINT check_app_settings_sentry_clientside_traces_sample_rate_range CHECK (((sentry_clientside_traces_sample_rate >= (0)::double precision) AND (sentry_clientside_traces_sample_rate <= (1)::double precision))),
     CONSTRAINT check_application_settings_clickhouse_is_hash CHECK ((jsonb_typeof(clickhouse) = 'object'::text)),
     CONSTRAINT check_application_settings_rate_limits_is_hash CHECK ((jsonb_typeof(rate_limits) = 'object'::text)),
+    CONSTRAINT check_application_settings_rate_limits_unauth_git_http_is_hash CHECK ((jsonb_typeof(rate_limits_unauthenticated_git_http) = 'object'::text)),
     CONSTRAINT check_application_settings_service_ping_settings_is_hash CHECK ((jsonb_typeof(service_ping_settings) = 'object'::text)),
     CONSTRAINT check_b8c74ea5b3 CHECK ((char_length(deactivation_email_additional_text) <= 1000)),
     CONSTRAINT check_cdfbd99405 CHECK ((char_length(security_txt_content) <= 2048)),
diff --git a/doc/administration/settings/git_http_rate_limits.md b/doc/administration/settings/git_http_rate_limits.md
new file mode 100644
index 0000000000000000000000000000000000000000..751e41eec9b4afd599c21515065dc590966cfa1b
--- /dev/null
+++ b/doc/administration/settings/git_http_rate_limits.md
@@ -0,0 +1,41 @@
+---
+stage: Create
+group: Source Code
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
+---
+
+# Rate limits on Git HTTP
+
+DETAILS:
+**Tier:** Free, Premium, Ultimate
+**Offering:** Self-managed, GitLab Dedicated
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147112) in GitLab 17.0.
+
+If you use Git HTTP in your repository,
+common Git operations can generate many Git HTTP requests.
+Some of these Git HTTP requests do not contain authentication parameter and
+are considered unauthenticated. You can enforce rate limits on Git HTTP requests.
+This can improve the security and durability of your web application.
+[General user and IP rate limits](../settings/user_and_ip_rate_limits.md) aren't applied
+to Git HTTP requests.
+
+## Configure Git HTTP rate limits
+
+Git HTTP rate limits are disabled by default. If enabled and configured, these limits
+are applied to Git HTTP requests.
+
+To configure Git HTTP rate limits:
+
+1. On the left sidebar, at the bottom, select **Admin Area**.
+1. Select **Settings > Network**.
+1. Expand **Git HTTP rate limits**.
+1. Select **Enable unauthenticated Git HTTP request rate limit**.
+1. Enter a value for **Max unauthenticated Git HTTP requests per period per user**.
+1. Enter a value for **Unauthenticated Git HTTP rate limit period in seconds**.
+1. Select **Save changes**.
+
+## Related topics
+
+- [Rate limiting](../../security/rate_limits.md)
+- [User and IP rate limits](../settings/user_and_ip_rate_limits.md)
diff --git a/doc/administration/settings/rate_limits.md b/doc/administration/settings/rate_limits.md
index 89e64344e0c3f60fb04383f93308c7bd771b670d..3bcd47ebdc3103189cfaaa4f19353da95b5d30ae 100644
--- a/doc/administration/settings/rate_limits.md
+++ b/doc/administration/settings/rate_limits.md
@@ -9,6 +9,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
 You can change network settings to limit the rate of connections with your instance.
 
 - [Deprecated API rate limits](deprecated_api_rate_limits.md)
+- [Git HTTP](git_http_rate_limits.md)
 - [Git LFS](git_lfs_rate_limits.md)
 - [Git SSH operations](rate_limits_on_git_ssh_operations.md)
 - [Incident management](incident_management_rate_limits.md)
diff --git a/lib/gitlab/rack_attack.rb b/lib/gitlab/rack_attack.rb
index 2182f5b56e456150caaff5dd135e143ae1a64e6c..113c6dec6c1b9d0c37783f75c9033c4c153c4486 100644
--- a/lib/gitlab/rack_attack.rb
+++ b/lib/gitlab/rack_attack.rb
@@ -126,6 +126,16 @@ def self.throttle_definitions
         'throttle_authenticated_git_lfs' => ThrottleDefinition.new(
           Gitlab::Throttle.throttle_authenticated_git_lfs_options,
           ->(req) { req.throttled_identifer([:api]) if req.throttle_authenticated_git_lfs? }
+        ),
+        **throttle_definitions_unauthenticated_git_http
+      }
+    end
+
+    def self.throttle_definitions_unauthenticated_git_http
+      {
+        'throttle_unauthenticated_git_http' => ThrottleDefinition.new(
+          Gitlab::Throttle.throttle_unauthenticated_git_http_options,
+          ->(req) { req.ip if req.throttle_unauthenticated_git_http? }
         )
       }
     end
diff --git a/lib/gitlab/rack_attack/request.rb b/lib/gitlab/rack_attack/request.rb
index e45782b8be0b8b3d2f1857439bd37b1048d25d4a..63c0215c65a2b2828b200dcfb8040643ff515f44 100644
--- a/lib/gitlab/rack_attack/request.rb
+++ b/lib/gitlab/rack_attack/request.rb
@@ -100,6 +100,7 @@ def throttle_unauthenticated_api?
       def throttle_unauthenticated_web?
         (web_request? || frontend_request?) &&
           !should_be_skipped? &&
+          !git_path? &&
           # TODO: Column will be renamed in https://gitlab.com/gitlab-org/gitlab/-/issues/340031
           Gitlab::Throttle.settings.throttle_unauthenticated_enabled &&
           unauthenticated?
@@ -175,6 +176,12 @@ def throttle_authenticated_packages_api?
           Gitlab::Throttle.settings.throttle_authenticated_packages_api_enabled
       end
 
+      def throttle_unauthenticated_git_http?
+        git_path? &&
+          Gitlab::Throttle.settings.throttle_unauthenticated_git_http_enabled &&
+          unauthenticated?
+      end
+
       def throttle_authenticated_git_lfs?
         git_lfs_path? &&
           Gitlab::Throttle.settings.throttle_authenticated_git_lfs_enabled
@@ -242,6 +249,10 @@ def packages_api_path?
         matches?(::Gitlab::Regex::Packages::API_PATH_REGEX)
       end
 
+      def git_path?
+        matches?(::Gitlab::PathRegex.repository_git_route_regex)
+      end
+
       def git_lfs_path?
         matches?(::Gitlab::PathRegex.repository_git_lfs_route_regex)
       end
diff --git a/lib/gitlab/throttle.rb b/lib/gitlab/throttle.rb
index 384953533b5e9becdd686f42274eb3bb9fdf3025..1aef19f1e6a453699096f075f5953c17e225898f 100644
--- a/lib/gitlab/throttle.rb
+++ b/lib/gitlab/throttle.rb
@@ -71,6 +71,13 @@ def self.protected_paths_options
       { limit: limit_proc, period: period_proc }
     end
 
+    def self.throttle_unauthenticated_git_http_options
+      limit_proc = proc { |req| settings.throttle_unauthenticated_git_http_requests_per_period }
+      period_proc = proc { |req| settings.throttle_unauthenticated_git_http_period_in_seconds.seconds }
+
+      { limit: limit_proc, period: period_proc }
+    end
+
     def self.throttle_authenticated_git_lfs_options
       limit_proc = proc { |req| settings.throttle_authenticated_git_lfs_requests_per_period }
       period_proc = proc { |req| settings.throttle_authenticated_git_lfs_period_in_seconds.seconds }
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index acbed3ec35ee27f05ae2f7a9f021a4812a981c2d..d2872dc7f442824f5e47d6244bce946c804611af 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -13681,6 +13681,9 @@ msgstr ""
 msgid "Configure specific limits for Files API requests that supersede the general user and IP rate limits."
 msgstr ""
 
+msgid "Configure specific limits for Git HTTP requests."
+msgstr ""
+
 msgid "Configure specific limits for Git LFS requests that supersede the general user and IP rate limits."
 msgstr ""
 
@@ -19707,6 +19710,9 @@ msgstr ""
 msgid "Enable unauthenticated API request rate limit"
 msgstr ""
 
+msgid "Enable unauthenticated Git HTTP request rate limit"
+msgstr ""
+
 msgid "Enable unauthenticated web request rate limit"
 msgstr ""
 
@@ -23076,6 +23082,9 @@ msgstr ""
 msgid "Git"
 msgstr ""
 
+msgid "Git HTTP rate limits"
+msgstr ""
+
 msgid "Git LFS is not enabled on this GitLab server, contact your admin."
 msgstr ""
 
@@ -25671,6 +25680,9 @@ msgstr ""
 msgid "Helps reduce request volume for protected paths."
 msgstr ""
 
+msgid "Helps reduce unauthenticated Git HTTP request volume for git paths."
+msgstr ""
+
 msgid "Hi %{recipient_name},"
 msgstr ""
 
@@ -31379,6 +31391,9 @@ msgstr ""
 msgid "Maximum unauthenticated API requests per rate limit period per IP"
 msgstr ""
 
+msgid "Maximum unauthenticated Git HTTP requests per period per IP"
+msgstr ""
+
 msgid "Maximum unauthenticated web requests per rate limit period per IP"
 msgstr ""
 
@@ -54784,6 +54799,12 @@ msgstr ""
 msgid "Unauthenticated API rate limit period in seconds"
 msgstr ""
 
+msgid "Unauthenticated Git HTTP rate limit period in seconds"
+msgstr ""
+
+msgid "Unauthenticated Git HTTP request rate limit"
+msgstr ""
+
 msgid "Unauthenticated requests"
 msgstr ""
 
diff --git a/spec/lib/gitlab/rack_attack/request_spec.rb b/spec/lib/gitlab/rack_attack/request_spec.rb
index 92c9acb83cf67f7bd92480d4d7b397774cf96bb1..a076cb557a061d7c74d4d1e48d9278786327123a 100644
--- a/spec/lib/gitlab/rack_attack/request_spec.rb
+++ b/spec/lib/gitlab/rack_attack/request_spec.rb
@@ -248,6 +248,38 @@
     end
   end
 
+  describe '#throttle_unauthenticated_git_http?' do
+    let_it_be(:project) { create(:project) }
+
+    let(:git_clone_project_path_get_info_refs) { "/#{project.full_path}.git/info/refs?service=git-upload-pack" }
+    let(:git_clone_path_post_git_upload_pack) { "/#{project.full_path}.git/git-upload-pack" }
+
+    subject { request.throttle_unauthenticated_git_http? }
+
+    where(:path, :request_unauthenticated?, :application_setting_throttle_unauthenticated_git_http_enabled, :expected) do
+      ref(:git_clone_project_path_get_info_refs) | true  | true  | true
+      ref(:git_clone_project_path_get_info_refs) | false | true  | false
+      ref(:git_clone_project_path_get_info_refs) | true  | false | false
+      ref(:git_clone_project_path_get_info_refs) | false | false | false
+
+      ref(:git_clone_path_post_git_upload_pack)  | true  | true  | true
+      ref(:git_clone_path_post_git_upload_pack)  | false | false | false
+
+      '/users/sign_in'                           | true  | true  | false
+      '/users/sign_in'                           | false | false | false
+    end
+
+    with_them do
+      before do
+        stub_application_setting(throttle_unauthenticated_git_http_enabled: application_setting_throttle_unauthenticated_git_http_enabled)
+
+        allow(request).to receive(:unauthenticated?).and_return(request_unauthenticated?)
+      end
+
+      it { is_expected.to eq expected }
+    end
+  end
+
   describe '#protected_path?' do
     subject { request.protected_path? }
 
diff --git a/spec/requests/rack_attack_global_spec.rb b/spec/requests/rack_attack_global_spec.rb
index 28cb295d3edd1c8ba0d70171a260ed3b6849794f..56473dbc7d7faa1293c5920abedc051d2534867a 100644
--- a/spec/requests/rack_attack_global_spec.rb
+++ b/spec/requests/rack_attack_global_spec.rb
@@ -5,6 +5,7 @@
 RSpec.describe 'Rack Attack global throttles', :use_clean_rails_memory_store_caching,
   feature_category: :system_access do
   include RackAttackSpecHelpers
+  include WorkhorseHelpers
   include SessionHelpers
 
   let(:settings) { Gitlab::CurrentSettings.current_application_settings }
@@ -27,6 +28,8 @@
       throttle_unauthenticated_packages_api_period_in_seconds: 1,
       throttle_authenticated_packages_api_requests_per_period: 100,
       throttle_authenticated_packages_api_period_in_seconds: 1,
+      throttle_unauthenticated_git_http_requests_per_period: 100,
+      throttle_unauthenticated_git_http_period_in_seconds: 1,
       throttle_authenticated_git_lfs_requests_per_period: 100,
       throttle_authenticated_git_lfs_period_in_seconds: 1,
       throttle_unauthenticated_files_api_requests_per_period: 100,
@@ -697,6 +700,45 @@ def do_request
     end
   end
 
+  describe 'unauthenticated git http requests' do
+    let_it_be(:project) { create(:project, :repository, :public) }
+
+    let(:git_http_clone_path) { "/#{project.full_path}.git/info/refs?service=git-upload-pack" }
+
+    it_behaves_like 'rate-limited unauthenticated requests' do
+      let(:throttle_name) { 'throttle_unauthenticated_git_http' }
+      let(:throttle_setting_prefix) { 'throttle_unauthenticated_git_http' }
+      let(:url_that_does_not_require_authentication) { "/#{project.full_path}.git/info/refs?service=git-upload-pack" }
+      let(:url_that_is_not_matched) { '/users/sign_in' }
+      let(:headers) { WorkhorseHelpers.workhorse_internal_api_request_header }
+    end
+
+    context 'when authenticated' do
+      let_it_be(:user) { create(:user) }
+      let_it_be(:token) { create(:personal_access_token, user: user) }
+
+      let(:headers) { WorkhorseHelpers.workhorse_internal_api_request_header.merge(basic_auth_headers(user, token)) }
+
+      def do_request
+        get git_http_clone_path, headers: headers
+      end
+
+      before do
+        settings_to_set[:throttle_unauthenticated_git_http_requests_per_period] = requests_per_period
+        settings_to_set[:throttle_unauthenticated_git_http_period_in_seconds] = period_in_seconds
+        settings_to_set[:throttle_unauthenticated_git_http_enabled] = true
+        stub_application_setting(settings_to_set)
+      end
+
+      it 'rejects requests over the rate limit' do
+        (requests_per_period + 1).times do
+          do_request
+          expect(response).to have_gitlab_http_status(:ok)
+        end
+      end
+    end
+  end
+
   describe 'authenticated git lfs requests', :api do
     let_it_be(:project) { create(:project, :internal) }
     let_it_be(:user) { create(:user) }
diff --git a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
index 89ae165f3fab6bf709fa9750f5a063df9ad5264e..d15c88244f84b72620c1540b0b26865b11f40ac0 100644
--- a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
+++ b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
@@ -459,7 +459,10 @@ def reset_rack_attack
 # * requests_per_period
 # * period_in_seconds
 # * period
+# * headers
 RSpec.shared_examples 'rate-limited unauthenticated requests' do
+  let(:headers) { {} }
+
   before do
     # Set low limits
     settings_to_set[:"#{throttle_setting_prefix}_requests_per_period"] = requests_per_period
@@ -467,6 +470,10 @@ def reset_rack_attack
     travel_back
   end
 
+  def do_request
+    get url_that_does_not_require_authentication, headers: headers
+  end
+
   context 'when the throttle is enabled' do
     before do
       settings_to_set[:"#{throttle_setting_prefix}_enabled"] = true
@@ -476,12 +483,12 @@ def reset_rack_attack
     it 'rejects requests over the rate limit' do
       # At first, allow requests under the rate limit.
       requests_per_period.times do
-        get url_that_does_not_require_authentication
+        do_request
         expect(response).to have_gitlab_http_status(:ok)
       end
 
       # the last straw
-      expect_rejection { get url_that_does_not_require_authentication }
+      expect_rejection { do_request }
     end
 
     context 'with custom response text' do
@@ -492,37 +499,37 @@ def reset_rack_attack
       it 'rejects requests over the rate limit' do
         # At first, allow requests under the rate limit.
         requests_per_period.times do
-          get url_that_does_not_require_authentication
+          do_request
           expect(response).to have_gitlab_http_status(:ok)
         end
 
         # the last straw
-        expect_rejection { get url_that_does_not_require_authentication }
+        expect_rejection { do_request }
         expect(response.body).to eq("Custom response\n")
       end
     end
 
     it 'allows requests after throttling and then waiting for the next period' do
       requests_per_period.times do
-        get url_that_does_not_require_authentication
+        do_request
         expect(response).to have_gitlab_http_status(:ok)
       end
 
-      expect_rejection { get url_that_does_not_require_authentication }
+      expect_rejection { do_request }
 
       travel_to(period.from_now) do
         requests_per_period.times do
-          get url_that_does_not_require_authentication
+          do_request
           expect(response).to have_gitlab_http_status(:ok)
         end
 
-        expect_rejection { get url_that_does_not_require_authentication }
+        expect_rejection { do_request }
       end
     end
 
     it 'counts requests from different IPs separately' do
       requests_per_period.times do
-        get url_that_does_not_require_authentication
+        do_request
         expect(response).to have_gitlab_http_status(:ok)
       end
 
@@ -531,7 +538,7 @@ def reset_rack_attack
       end
 
       # would be over limit for the same IP
-      get url_that_does_not_require_authentication
+      do_request
       expect(response).to have_gitlab_http_status(:ok)
     end
 
@@ -603,7 +610,7 @@ def reset_rack_attack
 
     it 'logs RackAttack info into structured logs' do
       requests_per_period.times do
-        get url_that_does_not_require_authentication
+        do_request
         expect(response).to have_gitlab_http_status(:ok)
       end
 
@@ -619,12 +626,12 @@ def reset_rack_attack
 
       expect(Gitlab::AuthLogger).to receive(:error).with(arguments)
 
-      get url_that_does_not_require_authentication
+      do_request
     end
 
     it_behaves_like 'tracking when dry-run mode is set' do
       def do_request
-        get url_that_does_not_require_authentication
+        get url_that_does_not_require_authentication, headers: headers
       end
     end
   end
@@ -637,7 +644,7 @@ def do_request
 
     it 'allows requests over the rate limit' do
       (1 + requests_per_period).times do
-        get url_that_does_not_require_authentication
+        do_request
         expect(response).to have_gitlab_http_status(:ok)
       end
     end
diff --git a/spec/views/admin/application_settings/network.html.haml_spec.rb b/spec/views/admin/application_settings/network.html.haml_spec.rb
index 193ee8a32d5adee5d94c6e86264bdf224b1a1211..defaa97f82ab616c84455421f42ef2fe02d7865a 100644
--- a/spec/views/admin/application_settings/network.html.haml_spec.rb
+++ b/spec/views/admin/application_settings/network.html.haml_spec.rb
@@ -11,6 +11,16 @@
     allow(view).to receive(:current_user) { admin }
   end
 
+  context 'for Git HTTP rate limit' do
+    it 'renders the `git_http_rate_limit_unauthenticated` field' do
+      render
+
+      expect(rendered).to have_field('application_setting_throttle_unauthenticated_git_http_enabled')
+      expect(rendered).to have_field('application_setting_throttle_unauthenticated_git_http_requests_per_period')
+      expect(rendered).to have_field('application_setting_throttle_unauthenticated_git_http_period_in_seconds')
+    end
+  end
+
   context 'for Projects API rate limit' do
     it 'renders the `projects_api_rate_limit_unauthenticated` field' do
       render