diff --git a/lib/api/ci/helpers/runner.rb b/lib/api/ci/helpers/runner.rb
index 7dd3d80445043c9ef7baed4e7073914c9fc7cb86..429f6a4627d5748a5704d2daae8ca176cdf940d8 100644
--- a/lib/api/ci/helpers/runner.rb
+++ b/lib/api/ci/helpers/runner.rb
@@ -12,6 +12,9 @@ module Runner
         JOB_TOKEN_PARAM = :token
         LEGACY_SYSTEM_XID = '<legacy>'
 
+        # TODO: Remove once https://gitlab.com/gitlab-org/gitlab/-/issues/504277 is closed.
+        UnknownRunnerOwnerError = Class.new(StandardError)
+
         def authenticate_runner!(ensure_runner_manager: true, update_contacted_at: true)
           track_runner_authentication
           forbidden! unless current_runner
@@ -53,6 +56,9 @@ def current_runner
         end
 
         def current_runner_manager
+          # NOTE: Avoid orphaned runners, since we're not allowed to created records with a nil sharding_key_id
+          raise UnknownRunnerOwnerError if !current_runner.instance_type? && current_runner.sharding_key_id.nil?
+
           strong_memoize(:current_runner_manager) do
             system_xid = params.fetch(:system_id, LEGACY_SYSTEM_XID)
             current_runner&.ensure_manager(system_xid)
diff --git a/lib/api/ci/runner.rb b/lib/api/ci/runner.rb
index d578405fe3376bf40b8b6558f5e3795d8738b5cd..43c03fd01aaefde8c21b8ed6d2e5e1881036ddc3 100644
--- a/lib/api/ci/runner.rb
+++ b/lib/api/ci/runner.rb
@@ -113,7 +113,7 @@ class Runner < ::API::Base
         desc 'Validate authentication credentials' do
           summary "Verify authentication for a registered runner"
           success Entities::Ci::RunnerRegistrationDetails
-          http_codes [[200, 'Credentials are valid'], [403, 'Forbidden']]
+          http_codes [[200, 'Credentials are valid'], [403, 'Forbidden'], [422, 'Runner is orphaned']]
         end
         params do
           requires :token, type: String, desc: "The runner's authentication token"
@@ -128,6 +128,8 @@ class Runner < ::API::Base
           status 200
 
           present current_runner, with: Entities::Ci::RunnerRegistrationDetails
+        rescue ::API::Ci::Helpers::Runner::UnknownRunnerOwnerError
+          unprocessable_entity!('Runner is orphaned')
         end
 
         desc 'Reset runner authentication token with current token' do
@@ -153,7 +155,8 @@ class Runner < ::API::Base
           http_codes [[201, 'Job was scheduled'],
                       [204, 'No job for Runner'],
                       [403, 'Forbidden'],
-                      [409, 'Conflict']]
+                      [409, 'Conflict'],
+                      [422, 'Runner is orphaned']]
         end
         params do
           requires :token, type: String, desc: "Runner's authentication token"
@@ -225,6 +228,8 @@ class Runner < ::API::Base
             Gitlab::Metrics.add_event(:build_invalid)
             conflict!
           end
+        rescue ::API::Ci::Helpers::Runner::UnknownRunnerOwnerError
+          unprocessable_entity!('Runner is orphaned')
         end
 
         desc 'Update a job' do
diff --git a/spec/lib/api/ci/helpers/runner_spec.rb b/spec/lib/api/ci/helpers/runner_spec.rb
index cecafc04e7e082ab37cf5c4a4cd7a8a52680f1f2..c98781c5fde5a50628a57c736a0af6dae7308cfb 100644
--- a/spec/lib/api/ci/helpers/runner_spec.rb
+++ b/spec/lib/api/ci/helpers/runner_spec.rb
@@ -107,14 +107,20 @@
         expect(current_runner_manager.contacted_at).to eq 1.hour.ago
       end
 
-      context 'when a runner manager with nil sharding_key_id already exists' do
-        before do
+      # TODO Remove when https://gitlab.com/gitlab-org/gitlab/-/issues/503749 is merged
+      context 'with nil sharding_key_id' do
+        let!(:existing_runner_manager) do
           Ci::ApplicationRecord.connection.execute <<~SQL
-            ALTER TABLE group_type_ci_runner_machines_687967fa8a
-              DROP CONSTRAINT IF EXISTS check_sharding_key_id_nullness
+            ALTER TABLE ci_runner_machines DISABLE TRIGGER ALL;
+
+            INSERT INTO ci_runner_machines
+               (created_at, updated_at, contacted_at, runner_id, runner_type, system_xid, sharding_key_id)
+              VALUES(NOW(), NOW(), '#{1.hour.ago}', #{runner.id}, 2, 'bar', NULL);
+
+            ALTER TABLE ci_runner_machines ENABLE TRIGGER ALL;
           SQL
 
-          existing_runner_manager.update_columns(sharding_key_id: nil)
+          Ci::RunnerManager.for_runner(runner).with_system_xid('bar').first
         end
 
         it 'reuses existing runner manager', :aggregate_failures do
@@ -161,6 +167,28 @@
         expect(current_runner_manager.sharding_key_id).to eq(runner.sharding_key_id)
       end
     end
+
+    context 'when project runner is missing sharding_key_id' do
+      let(:runner) { Ci::Runner.project_type.last }
+      let(:connection) { Ci::Runner.connection }
+
+      before do
+        connection.execute(<<~SQL)
+          ALTER TABLE ci_runners DISABLE TRIGGER ALL;
+
+          INSERT INTO ci_runners(created_at, runner_type, token, sharding_key_id) VALUES(NOW(), 3, 'foo', NULL);
+
+          ALTER TABLE ci_runners ENABLE TRIGGER ALL;
+        SQL
+      end
+
+      it 'fails to create a new runner manager', :aggregate_failures do
+        allow(helper).to receive(:params).and_return(token: runner.token, system_id: 'new_system_id')
+        expect(helper.current_runner).to eq(runner)
+
+        expect { current_runner_manager }.to raise_error described_class::UnknownRunnerOwnerError
+      end
+    end
   end
 
   describe '#track_runner_authentication', :prometheus, feature_category: :runner do
diff --git a/spec/requests/api/ci/runner/jobs_request_post_spec.rb b/spec/requests/api/ci/runner/jobs_request_post_spec.rb
index 5d5dbda04fa79763c2d610bf197a4f71c590ce11..8b579fa3d3f825d97b54708b91d87ef0673ac79b 100644
--- a/spec/requests/api/ci/runner/jobs_request_post_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_request_post_spec.rb
@@ -182,6 +182,24 @@
                 expect(non_partitioned_runner.contacted_at).to be_nil
               end
 
+              context 'when project runner is missing sharding_key_id' do
+                let(:params) { { token: 'foo' } }
+                let(:runner) { Ci::Runner.project_type.last }
+                let(:non_partitioned_runner) do
+                  connection.execute(<<~SQL)
+                    INSERT INTO ci_runners(created_at, runner_type, token, sharding_key_id) VALUES(NOW(), 3, 'foo', NULL);
+                  SQL
+
+                  runner
+                end
+
+                it 'returns unprocessable entity status code', :aggregate_failures do
+                  expect { request }.not_to change { Ci::RunnerManager.count }.from(0)
+                  expect(response).to have_gitlab_http_status(:unprocessable_entity)
+                  expect(response.body).to eq({ message: 'Runner is orphaned' }.to_json)
+                end
+              end
+
               private
 
               def partitioned_runner_exists?(runner)
diff --git a/spec/requests/api/ci/runner/runners_verify_post_spec.rb b/spec/requests/api/ci/runner/runners_verify_post_spec.rb
index 192e8d88a4dbe0d0259a170375b45ba5caddcde4..39323c8f988859afb3e55dc98a52e0cf4359fab6 100644
--- a/spec/requests/api/ci/runner/runners_verify_post_spec.rb
+++ b/spec/requests/api/ci/runner/runners_verify_post_spec.rb
@@ -159,6 +159,29 @@ def partitioned_runner_exists?(runner)
           it 'creates a runner_manager' do
             expect { verify }.to change { Ci::RunnerManager.count }.by(1)
           end
+
+          # TODO: Remove once https://gitlab.com/gitlab-org/gitlab/-/issues/504277 is closed.
+          context 'when project runner is missing sharding_key_id' do
+            let(:runner) { Ci::Runner.project_type.last }
+            let(:params) { { token: 'foo' } }
+            let(:connection) { Ci::Runner.connection }
+
+            before do
+              connection.execute(<<~SQL)
+                ALTER TABLE ci_runners DISABLE TRIGGER ALL;
+
+                INSERT INTO ci_runners(created_at, runner_type, token, sharding_key_id) VALUES(NOW(), 3, 'foo', NULL);
+
+                ALTER TABLE ci_runners ENABLE TRIGGER ALL;
+              SQL
+            end
+
+            it 'returns unprocessable entity status code', :aggregate_failures do
+              expect { verify }.not_to change { Ci::RunnerManager.count }.from(0)
+              expect(response).to have_gitlab_http_status(:unprocessable_entity)
+              expect(response.body).to eq({ message: 'Runner is orphaned' }.to_json)
+            end
+          end
         end
       end