diff --git a/Gemfile b/Gemfile
index f7d1a2a874056d2a8b5f66c4d57fbee95fd35a8b..f9507d71e4e67fa3c6c20d45592957976c640e62 100644
--- a/Gemfile
+++ b/Gemfile
@@ -275,7 +275,7 @@ gem 'rack', '~> 2.2.9' # rubocop:todo Gemfile/MissingFeatureCategory
 gem 'rack-timeout', '~> 0.7.0', require: 'rack/timeout/base' # rubocop:todo Gemfile/MissingFeatureCategory
 
 group :puma do
-  gem 'puma', '= 6.4.0', require: false, feature_category: :shared
+  gem 'puma', '= 6.4.3', require: false, feature_category: :shared
   gem 'sd_notify', '~> 0.1.0', require: false # rubocop:todo Gemfile/MissingFeatureCategory
 end
 
diff --git a/Gemfile.checksum b/Gemfile.checksum
index b10fb5b6958e067f0071713d3b941c04439eba2f..5b566aa8fedcbf43014d21e778cfc44bf1c44a63 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -525,8 +525,8 @@
 {"name":"pry-rails","version":"0.3.9","platform":"ruby","checksum":"468662575abb6b67f4a9831219f99290d5eae7bf186e64dd810d0a3e4a8cc4b1"},
 {"name":"pry-shell","version":"0.6.4","platform":"ruby","checksum":"ad024882d29912b071a7de65ebea538b242d2dc1498c60c7c2352ef94769f208"},
 {"name":"public_suffix","version":"6.0.1","platform":"ruby","checksum":"61d44e1cab5cbbbe5b31068481cf16976dd0dc1b6b07bd95617ef8c5e3e00c6f"},
-{"name":"puma","version":"6.4.0","platform":"java","checksum":"eb27679e9e665882bab85dfa84704b0615b4f77cec46de014f05b90a5ab36cfe"},
-{"name":"puma","version":"6.4.0","platform":"ruby","checksum":"d5dda11362744df9f4694708a62e3cfddf72eba7498c16016ebbb30f106712f9"},
+{"name":"puma","version":"6.4.3","platform":"java","checksum":"373fcfacacaafd0f5a24db18cb99b3f2decb5c5316470169852559aa80adc8ab"},
+{"name":"puma","version":"6.4.3","platform":"ruby","checksum":"24a4645c006811d83f2480057d1f54a96e7627b6b90e1c99b260b9dc630eb43e"},
 {"name":"pyu-ruby-sasl","version":"0.0.3.3","platform":"ruby","checksum":"5683a6bc5738db5a1bf5ceddeaf545405fb241b4184dd4f2587e679a7e9497e5"},
 {"name":"raabro","version":"1.4.0","platform":"ruby","checksum":"d4fa9ff5172391edb92b242eed8be802d1934b1464061ae5e70d80962c5da882"},
 {"name":"racc","version":"1.6.2","platform":"java","checksum":"0880781e7dfde09e665d0b6160b583e01ed52fcc2955d7891447d33c2d1d2cf1"},
diff --git a/Gemfile.lock b/Gemfile.lock
index dc42b2d8cc66416c571f3f539ebf73c99ee3659d..5ef32293dab666eefebf103555461db179b74b01 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1451,7 +1451,7 @@ GEM
       tty-markdown
       tty-prompt
     public_suffix (6.0.1)
-    puma (6.4.0)
+    puma (6.4.3)
       nio4r (~> 2.0)
     pyu-ruby-sasl (0.0.3.3)
     raabro (1.4.0)
@@ -2232,7 +2232,7 @@ DEPENDENCIES
   pry-byebug
   pry-rails (~> 0.3.9)
   pry-shell (~> 0.6.4)
-  puma (= 6.4.0)
+  puma (= 6.4.3)
   rack (~> 2.2.9)
   rack-attack (~> 6.7.0)
   rack-cors (~> 2.0.1)
diff --git a/Gemfile.next.checksum b/Gemfile.next.checksum
index 3c3bca8eac0e68a3083deb260671a9a33734320a..f290ce5989e0ef7ec6fdb68a438ce9184b5f5ab5 100644
--- a/Gemfile.next.checksum
+++ b/Gemfile.next.checksum
@@ -535,8 +535,8 @@
 {"name":"psych","version":"5.1.2","platform":"java","checksum":"1dd68dc609eddbc884e6892e11da942e16f7256bd30ebde9d35449d43043a6fe"},
 {"name":"psych","version":"5.1.2","platform":"ruby","checksum":"337322f58fc2bf24827d2b9bd5ab595f6a72971867d151bb39980060ea40a368"},
 {"name":"public_suffix","version":"6.0.1","platform":"ruby","checksum":"61d44e1cab5cbbbe5b31068481cf16976dd0dc1b6b07bd95617ef8c5e3e00c6f"},
-{"name":"puma","version":"6.4.0","platform":"java","checksum":"eb27679e9e665882bab85dfa84704b0615b4f77cec46de014f05b90a5ab36cfe"},
-{"name":"puma","version":"6.4.0","platform":"ruby","checksum":"d5dda11362744df9f4694708a62e3cfddf72eba7498c16016ebbb30f106712f9"},
+{"name":"puma","version":"6.4.3","platform":"java","checksum":"373fcfacacaafd0f5a24db18cb99b3f2decb5c5316470169852559aa80adc8ab"},
+{"name":"puma","version":"6.4.3","platform":"ruby","checksum":"24a4645c006811d83f2480057d1f54a96e7627b6b90e1c99b260b9dc630eb43e"},
 {"name":"pyu-ruby-sasl","version":"0.0.3.3","platform":"ruby","checksum":"5683a6bc5738db5a1bf5ceddeaf545405fb241b4184dd4f2587e679a7e9497e5"},
 {"name":"raabro","version":"1.4.0","platform":"ruby","checksum":"d4fa9ff5172391edb92b242eed8be802d1934b1464061ae5e70d80962c5da882"},
 {"name":"racc","version":"1.6.2","platform":"java","checksum":"0880781e7dfde09e665d0b6160b583e01ed52fcc2955d7891447d33c2d1d2cf1"},
diff --git a/Gemfile.next.lock b/Gemfile.next.lock
index cecfa8787a49e1364dbf36aba9277f9cbc046de1..bad9b5a7eb9c25f592844cb5d37465eb76bd0a29 100644
--- a/Gemfile.next.lock
+++ b/Gemfile.next.lock
@@ -1456,7 +1456,7 @@ GEM
     psych (5.1.2)
       stringio
     public_suffix (6.0.1)
-    puma (6.4.0)
+    puma (6.4.3)
       nio4r (~> 2.0)
     pyu-ruby-sasl (0.0.3.3)
     raabro (1.4.0)
@@ -2247,7 +2247,7 @@ DEPENDENCIES
   pry-byebug
   pry-rails (~> 0.3.9)
   pry-shell (~> 0.6.4)
-  puma (= 6.4.0)
+  puma (= 6.4.3)
   rack (~> 2.2.9)
   rack-attack (~> 6.7.0)
   rack-cors (~> 2.0.1)
diff --git a/config/initializers/puma_patch.rb b/config/initializers/puma_patch.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9f8c690b9a0b6f8c75d35def7bd4961ddb8216d1
--- /dev/null
+++ b/config/initializers/puma_patch.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+return unless Gitlab::Runtime.puma?
+
+require 'puma'
+require 'puma/cluster'
+
+# Ruby 3.1 and 3.2 have bugs that prevents Puma from reaping child processes properly:
+# https://bugs.ruby-lang.org/issues/20490
+# https://bugs.ruby-lang.org/issues/19837
+#
+# https://github.com/puma/puma/pull/3314 fixes this in Puma, but a release
+# has not been forthcoming.
+if Gem::Version.new(Puma::Const::PUMA_VERSION) > Gem::Version.new('6.5')
+  raise 'This patch should not be needed after Puma 6.5.0.'
+end
+
+# rubocop:disable Style/RedundantBegin -- These are upstream changes
+# rubocop:disable Cop/LineBreakAfterGuardClauses -- These are upstream changes
+# rubocop:disable Layout/EmptyLineAfterGuardClause -- These are upstream changes
+module Puma
+  class Cluster < Runner
+    # loops thru @workers, removing workers that exited, and calling
+    # `#term` if needed
+    def wait_workers
+      # Reap all children, known workers or otherwise.
+      # If puma has PID 1, as it's common in containerized environments,
+      # then it's responsible for reaping orphaned processes, so we must reap
+      # all our dead children, regardless of whether they are workers we spawned
+      # or some reattached processes.
+      reaped_children = {}
+      loop do
+        begin
+          pid, status = Process.wait2(-1, Process::WNOHANG)
+          break unless pid
+          reaped_children[pid] = status
+        rescue Errno::ECHILD
+          break
+        end
+      end
+
+      @workers.reject! do |w|
+        next false if w.pid.nil?
+        begin
+          # We may need to check the PID individually because:
+          # 1. From Ruby versions 2.6 to 3.2, `Process.detach` can prevent or delay
+          #    `Process.wait2(-1)` from detecting a terminated process: https://bugs.ruby-lang.org/issues/19837.
+          # 2. When `fork_worker` is enabled, some worker may not be direct children,
+          #    but grand children.  Because of this they won't be reaped by `Process.wait2(-1)`.
+          if reaped_children.delete(w.pid) || Process.wait(w.pid, Process::WNOHANG)
+            true
+          else
+            w.term if w.term?
+            nil
+          end
+        rescue Errno::ECHILD
+          begin
+            Process.kill(0, w.pid)
+            # child still alive but has another parent (e.g., using fork_worker)
+            w.term if w.term?
+            false
+          rescue Errno::ESRCH, Errno::EPERM
+            true # child is already terminated
+          end
+        end
+      end
+
+      # Log unknown children
+      reaped_children.each do |pid, status|
+        log "! reaped unknown child process pid=#{pid} status=#{status}"
+      end
+    end
+  end
+end
+# rubocop:enable Style/RedundantBegin
+# rubocop:enable Cop/LineBreakAfterGuardClauses
+# rubocop:enable Layout/EmptyLineAfterGuardClause