diff --git a/.rubocop.yml b/.rubocop.yml
index a82910d89fbc1a7195a5a002d8e95a70381739f9..6a460e30a9553bc81614b3e127bfebbb315b66fb 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -712,3 +712,8 @@ QA/SelectorUsage:
     - 'ee/spec/**/*.rb'
   Exclude:
     - 'spec/rubocop/**/*_spec.rb'
+
+Performance/ActiveRecordSubtransactions:
+  Exclude:
+    - 'spec/**/*.rb'
+    - 'ee/spec/**/*.rb'
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index 29ef76547e0667e39c1b0b14360e6ccb13a09a63..2353a514097f0105d561318c70ccaa487723f427 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -31,7 +31,7 @@ def self.pluck_primary_key
   end
 
   def self.safe_ensure_unique(retries: 0)
-    transaction(requires_new: true) do
+    transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
       yield
     end
   rescue ActiveRecord::RecordNotUnique
@@ -55,7 +55,7 @@ def self.safe_find_or_create_by!(*args, &block)
   # currently one third of the default 15-second timeout
   def self.with_fast_read_statement_timeout(timeout_ms = 5000)
     ::Gitlab::Database::LoadBalancing::Session.current.fallback_to_replicas_for_ambiguous_queries do
-      transaction(requires_new: true) do
+      transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
         connection.exec_query("SET LOCAL statement_timeout = #{timeout_ms}")
 
         yield
@@ -80,7 +80,7 @@ def self.optimized_safe_find_or_create_by(*args, &block)
     #
     # When calling this method on an association, just calling `self.create` would call `ActiveRecord::Persistence.create`
     # and that skips some code that adds the newly created record to the association.
-    transaction(requires_new: true) { all.create(*args, &block) }
+    transaction(requires_new: true) { all.create(*args, &block) } # rubocop:disable Performance/ActiveRecordSubtransactions
   rescue ActiveRecord::RecordNotUnique
     find_by(*args)
   end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index c4b6bcb9395f000eb19deaac37d19d1fad3460a5..8ed408d2c23d0b064a06c742d8733a0031d781e1 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -622,7 +622,7 @@ def instance_review_permitted?
   def self.create_from_defaults
     check_schema!
 
-    transaction(requires_new: true) do
+    transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
       super
     end
   rescue ActiveRecord::RecordNotUnique
diff --git a/app/models/internal_id.rb b/app/models/internal_id.rb
index 107b1914af434214db93524304c30a19cb1959ef..9cbb5f92bbdd62b328ae3d4da7fe802f714f0ce9 100644
--- a/app/models/internal_id.rb
+++ b/app/models/internal_id.rb
@@ -239,7 +239,7 @@ def create_record
         lookup
       else
         begin
-          subject.transaction(requires_new: true) do
+          subject.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
             InternalId.create!(
               **scope,
               usage: usage_value,
@@ -362,7 +362,7 @@ def create_record!(subject, scope, usage, value)
         value
       else
         begin
-          subject.transaction(requires_new: true) do
+          subject.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
             internal_id = InternalId.create!(**scope, usage: usage, last_value: value)
             internal_id.last_value
           end
diff --git a/app/services/projects/move_deploy_keys_projects_service.rb b/app/services/projects/move_deploy_keys_projects_service.rb
index 51d84af249e25eab0317ca272564af7ae75fb979..17513f0ba11f86439755666b6500334c594c2969 100644
--- a/app/services/projects/move_deploy_keys_projects_service.rb
+++ b/app/services/projects/move_deploy_keys_projects_service.rb
@@ -5,7 +5,7 @@ class MoveDeployKeysProjectsService < BaseMoveRelationsService
     def execute(source_project, remove_remaining_elements: true)
       return unless super
 
-      Project.transaction(requires_new: true) do
+      Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
         move_deploy_keys_projects
         remove_remaining_deploy_keys_projects if remove_remaining_elements
 
diff --git a/app/services/projects/move_forks_service.rb b/app/services/projects/move_forks_service.rb
index 33f0bab12c95cecd1f23a8b85a9b4d0b50e2c5e6..f61552f661f95c6c98435e46cbb8c2f1b3df0797 100644
--- a/app/services/projects/move_forks_service.rb
+++ b/app/services/projects/move_forks_service.rb
@@ -5,7 +5,7 @@ class MoveForksService < BaseMoveRelationsService
     def execute(source_project, remove_remaining_elements: true)
       return unless super && source_project.fork_network
 
-      Project.transaction(requires_new: true) do
+      Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
         move_fork_network_members
         update_root_project
         refresh_forks_count
diff --git a/app/services/projects/move_lfs_objects_projects_service.rb b/app/services/projects/move_lfs_objects_projects_service.rb
index 57a8d3d69c6fb83507ebb25f01fa646ef17b51ff..ca8b4bfde68784e0e1967e14302f2f8fff21415c 100644
--- a/app/services/projects/move_lfs_objects_projects_service.rb
+++ b/app/services/projects/move_lfs_objects_projects_service.rb
@@ -5,7 +5,7 @@ class MoveLfsObjectsProjectsService < BaseMoveRelationsService
     def execute(source_project, remove_remaining_elements: true)
       return unless super
 
-      Project.transaction(requires_new: true) do
+      Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
         move_lfs_objects_projects
         remove_remaining_lfs_objects_project if remove_remaining_elements
 
diff --git a/app/services/projects/move_notification_settings_service.rb b/app/services/projects/move_notification_settings_service.rb
index efe06f158ccd22ceb8ba949b2b71245062262ea7..fc268f5762b883ee35f72f42d69d66810e225401 100644
--- a/app/services/projects/move_notification_settings_service.rb
+++ b/app/services/projects/move_notification_settings_service.rb
@@ -5,7 +5,7 @@ class MoveNotificationSettingsService < BaseMoveRelationsService
     def execute(source_project, remove_remaining_elements: true)
       return unless super
 
-      Project.transaction(requires_new: true) do
+      Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
         move_notification_settings
         remove_remaining_notification_settings if remove_remaining_elements
 
diff --git a/app/services/projects/move_project_authorizations_service.rb b/app/services/projects/move_project_authorizations_service.rb
index c95ad60ab5e229dabd6dcd97806b840701e08016..30f3b892131c1352c4d18c1404a6ee7112b160ee 100644
--- a/app/services/projects/move_project_authorizations_service.rb
+++ b/app/services/projects/move_project_authorizations_service.rb
@@ -9,7 +9,7 @@ class MoveProjectAuthorizationsService < BaseMoveRelationsService
     def execute(source_project, remove_remaining_elements: true)
       return unless super
 
-      Project.transaction(requires_new: true) do
+      Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
         move_project_authorizations
 
         remove_remaining_authorizations if remove_remaining_elements
diff --git a/app/services/projects/move_project_group_links_service.rb b/app/services/projects/move_project_group_links_service.rb
index 349953ff973d4de385f9d3e2370d74fd3e6e3166..8344b0b404b955197ebbcef1fe27333d8da7dc37 100644
--- a/app/services/projects/move_project_group_links_service.rb
+++ b/app/services/projects/move_project_group_links_service.rb
@@ -9,7 +9,7 @@ class MoveProjectGroupLinksService < BaseMoveRelationsService
     def execute(source_project, remove_remaining_elements: true)
       return unless super
 
-      Project.transaction(requires_new: true) do
+      Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
         move_group_links
         remove_remaining_project_group_links if remove_remaining_elements
 
diff --git a/app/services/projects/move_project_members_service.rb b/app/services/projects/move_project_members_service.rb
index 9a1b7c6d1b6239dc09c0d38b91339b6fff5d9ece..11629c3fd7e8eed34f40b987cf91c38e7fcc13d8 100644
--- a/app/services/projects/move_project_members_service.rb
+++ b/app/services/projects/move_project_members_service.rb
@@ -9,7 +9,7 @@ class MoveProjectMembersService < BaseMoveRelationsService
     def execute(source_project, remove_remaining_elements: true)
       return unless super
 
-      Project.transaction(requires_new: true) do
+      Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
         move_project_members
         remove_remaining_members if remove_remaining_elements
 
diff --git a/app/services/projects/move_users_star_projects_service.rb b/app/services/projects/move_users_star_projects_service.rb
index 20121d429e2f5711e9bdcf62d7cc97ea9f248ace..b8564a02301cf53892d948992a1e13cf9f5d04d3 100644
--- a/app/services/projects/move_users_star_projects_service.rb
+++ b/app/services/projects/move_users_star_projects_service.rb
@@ -9,7 +9,7 @@ def execute(source_project, remove_remaining_elements: true)
 
       return unless user_stars.any?
 
-      Project.transaction(requires_new: true) do
+      Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
         user_stars.update_all(project_id: @project.id)
 
         Project.reset_counters @project.id, :users_star_projects
diff --git a/app/services/users/migrate_to_ghost_user_service.rb b/app/services/users/migrate_to_ghost_user_service.rb
index a471f55e644a267ac4be53992dea703d52007071..09fd74e9818f674b67df55a137cc91e61563d0d0 100644
--- a/app/services/users/migrate_to_ghost_user_service.rb
+++ b/app/services/users/migrate_to_ghost_user_service.rb
@@ -39,7 +39,7 @@ def execute
     private
 
     def migrate_records_in_transaction
-      user.transaction(requires_new: true) do
+      user.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
         @ghost_user = User.ghost
 
         migrate_records
diff --git a/db/post_migrate/20201106134950_deduplicate_epic_iids.rb b/db/post_migrate/20201106134950_deduplicate_epic_iids.rb
index bc7daf9329d65b004588ceb6d2bb64deb35dfc2c..8fddc81057b6b929fffb0718136d6d7c31a98dff 100644
--- a/db/post_migrate/20201106134950_deduplicate_epic_iids.rb
+++ b/db/post_migrate/20201106134950_deduplicate_epic_iids.rb
@@ -85,7 +85,7 @@ def create_record
 
       instance = subject.is_a?(::Class) ? nil : subject
 
-      subject.transaction(requires_new: true) do
+      subject.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
         InternalId.create!(
           **scope,
           usage: usage_value,
diff --git a/ee/app/services/vulnerabilities/create_service.rb b/ee/app/services/vulnerabilities/create_service.rb
index e9b27bee2c75a2db2203a7739e246dfb666f3918..02aad1506e459a0b00523649e7cfe0d7d9a30d49 100644
--- a/ee/app/services/vulnerabilities/create_service.rb
+++ b/ee/app/services/vulnerabilities/create_service.rb
@@ -16,7 +16,7 @@ def execute
 
       vulnerability = Vulnerability.new
 
-      Vulnerabilities::Finding.transaction(requires_new: true) do
+      Vulnerabilities::Finding.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
         save_vulnerability(vulnerability, finding)
         Statistics::UpdateService.update_for(vulnerability)
         HistoricalStatistics::UpdateService.update_for(@project)
diff --git a/ee/lib/ee/gitlab/background_migration/migrate_approver_to_approval_rules.rb b/ee/lib/ee/gitlab/background_migration/migrate_approver_to_approval_rules.rb
index 62b0219e62fa0d3bea28d3d94d9f3f6fa4a8c199..10c77813d99775faed4d63e900afefb561090ef4 100644
--- a/ee/lib/ee/gitlab/background_migration/migrate_approver_to_approval_rules.rb
+++ b/ee/lib/ee/gitlab/background_migration/migrate_approver_to_approval_rules.rb
@@ -65,7 +65,7 @@ def self.safe_find_or_create_by(*args)
           end
 
           def self.safe_ensure_unique(retries: 0)
-            transaction(requires_new: true) do
+            transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
               yield
             end
           rescue ActiveRecord::RecordNotUnique
diff --git a/lib/gitlab/background_migration/backfill_design_internal_ids.rb b/lib/gitlab/background_migration/backfill_design_internal_ids.rb
index 6d1df95c66d920472000498d0fee219005770baa..236c6b6eb9a72990af3aa3f53e3274b44fac4a61 100644
--- a/lib/gitlab/background_migration/backfill_design_internal_ids.rb
+++ b/lib/gitlab/background_migration/backfill_design_internal_ids.rb
@@ -73,7 +73,7 @@ def usage_value
         # violation. We can safely roll-back the nested transaction and perform
         # a lookup instead to retrieve the record.
         def create_record
-          subject.transaction(requires_new: true) do
+          subject.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
             InternalId.create!(
               **scope,
               usage: usage_value,
diff --git a/lib/gitlab/background_migration/backfill_project_repositories.rb b/lib/gitlab/background_migration/backfill_project_repositories.rb
index f5c8796bd18ecb886da48a07c017d7395c795428..a9eaeb0562dc5df9bde7cb2d434287a60b6a1336 100644
--- a/lib/gitlab/background_migration/backfill_project_repositories.rb
+++ b/lib/gitlab/background_migration/backfill_project_repositories.rb
@@ -21,7 +21,7 @@ def find_shard_id(name)
           shard_id = shards.fetch(name, nil)
           return shard_id if shard_id.present?
 
-          Shard.transaction(requires_new: true) do
+          Shard.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
             create!(name)
           end
         rescue ActiveRecord::RecordNotUnique
diff --git a/lib/gitlab/database/with_lock_retries.rb b/lib/gitlab/database/with_lock_retries.rb
index bbf8f133f0fc722ebfbd75ae2729ecccccb63dea..70acbeac9e151f24bbd5c6216b90f2c1da55dd95 100644
--- a/lib/gitlab/database/with_lock_retries.rb
+++ b/lib/gitlab/database/with_lock_retries.rb
@@ -122,7 +122,7 @@ def run_block
       end
 
       def run_block_with_lock_timeout
-        ActiveRecord::Base.transaction(requires_new: true) do
+        ActiveRecord::Base.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
           execute("SET LOCAL lock_timeout TO '#{current_lock_timeout_in_ms}ms'")
 
           log(message: 'Lock timeout is set', current_iteration: current_iteration, lock_timeout_in_ms: current_lock_timeout_in_ms)
diff --git a/rubocop/cop/performance/active_record_subtransactions.rb b/rubocop/cop/performance/active_record_subtransactions.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a550b558e52a1cbb43b495d85fb860a5bc4a4ce5
--- /dev/null
+++ b/rubocop/cop/performance/active_record_subtransactions.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module RuboCop
+  module Cop
+    module Performance
+      class ActiveRecordSubtransactions < RuboCop::Cop::Cop
+        MSG = 'Subtransactions should not be used. ' \
+          'For more information see: https://gitlab.com/gitlab-org/gitlab/-/issues/338346'
+
+        def_node_matcher :match_transaction_with_options, <<~PATTERN
+          (send _ :transaction (hash $...))
+        PATTERN
+
+        def_node_matcher :subtransaction_option?, <<~PATTERN
+          (pair (:sym :requires_new) (true))
+        PATTERN
+
+        def on_send(node)
+          match_transaction_with_options(node) do |option_nodes|
+            option_nodes.each do |option_node|
+              next unless subtransaction_option?(option_node)
+
+              add_offense(option_node)
+            end
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/spec/rubocop/cop/performance/active_record_subtransactions_spec.rb b/spec/rubocop/cop/performance/active_record_subtransactions_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0da2e30062a2fb88abdc6cf102e8cf939bcd2dd4
--- /dev/null
+++ b/spec/rubocop/cop/performance/active_record_subtransactions_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require_relative '../../../../rubocop/cop/performance/active_record_subtransactions'
+
+RSpec.describe RuboCop::Cop::Performance::ActiveRecordSubtransactions do
+  subject(:cop) { described_class.new }
+
+  let(:message) { described_class::MSG }
+
+  context 'when calling #transaction with only requires_new: true' do
+    it 'registers an offense' do
+      expect_offense(<<~RUBY)
+        ApplicationRecord.transaction(requires_new: true) do
+                                      ^^^^^^^^^^^^^^^^^^ #{message}
+          Project.create!(name: 'MyProject')
+        end
+      RUBY
+    end
+  end
+
+  context 'when passing multiple arguments to #transaction, including requires_new: true' do
+    it 'registers an offense' do
+      expect_offense(<<~RUBY)
+        ApplicationRecord.transaction(isolation: :read_committed, requires_new: true) do
+                                                                  ^^^^^^^^^^^^^^^^^^ #{message}
+          Project.create!(name: 'MyProject')
+        end
+      RUBY
+    end
+  end
+
+  context 'when calling #transaction with requires_new: false' do
+    it 'does not register an offense' do
+      expect_no_offenses(<<~RUBY)
+        ApplicationRecord.transaction(requires_new: false) do
+          Project.create!(name: 'MyProject')
+        end
+      RUBY
+    end
+  end
+
+  context 'when calling #transaction with other options' do
+    it 'does not register an offense' do
+      expect_no_offenses(<<~RUBY)
+        ApplicationRecord.transaction(isolation: :read_committed) do
+          Project.create!(name: 'MyProject')
+        end
+      RUBY
+    end
+  end
+
+  context 'when calling #transaction with no arguments' do
+    it 'does not register an offense' do
+      expect_no_offenses(<<~RUBY)
+        ApplicationRecord.transaction do
+          Project.create!(name: 'MyProject')
+        end
+      RUBY
+    end
+  end
+end