diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index bab10606faf8690e9002e0afb52b87351872738a..c6fd345acee240054ff8acef06c3ed344bbefd47 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -224,6 +224,7 @@ class Runner < Ci::ApplicationRecord
     scope :with_creator, -> { preload(:creator) }
 
     validate :tag_constraints
+    validates :sharding_key_id, presence: true, on: :create, unless: :instance_type?
     validates :name, length: { maximum: 256 }, if: :name_changed?
     validates :description, length: { maximum: 1024 }, if: :description_changed?
     validates :access_level, presence: true
@@ -231,6 +232,7 @@ class Runner < Ci::ApplicationRecord
     validates :registration_type, presence: true
 
     validate :no_projects, unless: :project_type?
+    validate :no_sharding_key_id, if: :instance_type?
     validate :no_groups, unless: :group_type?
     validate :any_project, if: :project_type?
     validate :exactly_one_group, if: :group_type?
@@ -354,6 +356,7 @@ def assign_to(project, current_user = nil)
 
       begin
         transaction do
+          self.sharding_key_id = project.id if self.runner_projects.empty?
           self.runner_projects << ::Ci::RunnerProject.new(project: project, runner: self)
           self.save!
         end
@@ -571,6 +574,12 @@ def tag_constraints
       end
     end
 
+    def no_sharding_key_id
+      if sharding_key_id
+        errors.add(:runner, 'cannot have sharding_key_id assigned')
+      end
+    end
+
     def no_projects
       if runner_projects.any?
         errors.add(:runner, 'cannot have projects assigned')
diff --git a/app/services/ci/runners/register_runner_service.rb b/app/services/ci/runners/register_runner_service.rb
index da9c555e8fb0ce12a981a8b812547bf535dc25c1..b717dab1b3700c75a18e4cb61b5903fea69d577a 100644
--- a/app/services/ci/runners/register_runner_service.rb
+++ b/app/services/ci/runners/register_runner_service.rb
@@ -44,10 +44,10 @@ def attrs_from_token
           { runner_type: :instance_type }
         elsif runner_registrar_valid?('project') && project = ::Project.find_by_runners_token(registration_token)
           # Create a project runner
-          { runner_type: :project_type, projects: [project] }
+          { runner_type: :project_type, projects: [project], sharding_key_id: project.id }
         elsif runner_registrar_valid?('group') && group = ::Group.find_by_runners_token(registration_token)
           # Create a group runner
-          { runner_type: :group_type, groups: [group] }
+          { runner_type: :group_type, groups: [group], sharding_key_id: group.id }
         elsif registration_token.present? && !Gitlab::CurrentSettings.allow_runner_registration_token
           {} # Will result in a :runner_registration_disallowed response
         end
diff --git a/app/services/ci/runners/runner_creation_strategies/group_runner_strategy.rb b/app/services/ci/runners/runner_creation_strategies/group_runner_strategy.rb
index 2eae5069046b74b957789f1b9b0e085e33e36846..1c32d3f3d357a3d8214cfb52d4af16adf0c93f57 100644
--- a/app/services/ci/runners/runner_creation_strategies/group_runner_strategy.rb
+++ b/app/services/ci/runners/runner_creation_strategies/group_runner_strategy.rb
@@ -12,8 +12,11 @@ def initialize(user:, params:)
         end
 
         def normalize_params
-          params[:runner_type] = 'group_type'
-          params[:groups] = [scope]
+          params.merge!({
+            runner_type: 'group_type',
+            sharding_key_id: scope&.id,
+            groups: [scope]
+          })
         end
 
         def validate_params
diff --git a/app/services/ci/runners/runner_creation_strategies/project_runner_strategy.rb b/app/services/ci/runners/runner_creation_strategies/project_runner_strategy.rb
index 487da9965133b214ed98e6c4b798901fd7865f5e..e8649bd8b6545451012d5e3c995beae0ebad47fa 100644
--- a/app/services/ci/runners/runner_creation_strategies/project_runner_strategy.rb
+++ b/app/services/ci/runners/runner_creation_strategies/project_runner_strategy.rb
@@ -12,8 +12,11 @@ def initialize(user:, params:)
         end
 
         def normalize_params
-          params[:runner_type] = 'project_type'
-          params[:projects] = [scope]
+          params.merge!({
+            runner_type: 'project_type',
+            sharding_key_id: scope&.id,
+            projects: [scope]
+          })
         end
 
         def validate_params
diff --git a/db/docs/batched_background_migrations/backfill_sharding_key_id_on_ci_runners.yml b/db/docs/batched_background_migrations/backfill_sharding_key_id_on_ci_runners.yml
new file mode 100644
index 0000000000000000000000000000000000000000..640aba92d7b570f1c750088973bf618e27bdb736
--- /dev/null
+++ b/db/docs/batched_background_migrations/backfill_sharding_key_id_on_ci_runners.yml
@@ -0,0 +1,10 @@
+---
+migration_job_name: BackfillShardingKeyIdOnCiRunners
+description: >-
+  Backfills the `sharding_key_id` column from `ci_runner_namespaces` and `ci_runner_projects`.
+  The column will serve as the sharding key in the future partitioned (by `runner_type`) table.
+feature_category: runner
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/166916
+milestone: '17.5'
+queued_migration_version: 20240923132401
+finalized_by: # version of the migration that finalized this BBM
diff --git a/db/migrate/20240923130542_add_sharding_key_id_to_ci_runners.rb b/db/migrate/20240923130542_add_sharding_key_id_to_ci_runners.rb
new file mode 100644
index 0000000000000000000000000000000000000000..45c1c6b478e3d82a7e81f0b953d677502fa66220
--- /dev/null
+++ b/db/migrate/20240923130542_add_sharding_key_id_to_ci_runners.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class AddShardingKeyIdToCiRunners < Gitlab::Database::Migration[2.2]
+  milestone '17.5'
+  enable_lock_retries!
+
+  def up
+    add_column :ci_runners, :sharding_key_id, :bigint, null: true, if_not_exists: true
+  end
+
+  def down
+    remove_column :ci_runners, :sharding_key_id, if_exists: true
+  end
+end
diff --git a/db/post_migrate/20240923132401_queue_backfill_sharding_key_id_on_ci_runners.rb b/db/post_migrate/20240923132401_queue_backfill_sharding_key_id_on_ci_runners.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b48fb9a1e8d1d0541582195fd37d6ff6085350fe
--- /dev/null
+++ b/db/post_migrate/20240923132401_queue_backfill_sharding_key_id_on_ci_runners.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+class QueueBackfillShardingKeyIdOnCiRunners < Gitlab::Database::Migration[2.2]
+  milestone '17.5'
+
+  restrict_gitlab_migration gitlab_schema: :gitlab_ci
+
+  MIGRATION = 'BackfillShardingKeyIdOnCiRunners'
+  DELAY_INTERVAL = 2.minutes
+  BATCH_SIZE = 1000
+  SUB_BATCH_SIZE = 100
+
+  def up
+    queue_batched_background_migration(
+      MIGRATION,
+      :ci_runners,
+      :id,
+      job_interval: DELAY_INTERVAL,
+      batch_size: BATCH_SIZE,
+      sub_batch_size: SUB_BATCH_SIZE
+    )
+  end
+
+  def down
+    delete_batched_background_migration(MIGRATION, :ci_runners, :id, [])
+  end
+end
diff --git a/db/schema_migrations/20240923130542 b/db/schema_migrations/20240923130542
new file mode 100644
index 0000000000000000000000000000000000000000..a915bcb6482725f56b551e90da7601fffb1012b4
--- /dev/null
+++ b/db/schema_migrations/20240923130542
@@ -0,0 +1 @@
+6aea04fdb5dc8a8753d73cfd4c20ecdf4747875460f9081cf9c0f8b9013d2c7c
\ No newline at end of file
diff --git a/db/schema_migrations/20240923132401 b/db/schema_migrations/20240923132401
new file mode 100644
index 0000000000000000000000000000000000000000..cc6cf707b42790de0b2f81e4472277030cc42bce
--- /dev/null
+++ b/db/schema_migrations/20240923132401
@@ -0,0 +1 @@
+9a9d6e31fdbf283db7c22fbd0f7e4dafb6e745dab183330591c4e3e3bb901acd
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 73d5f122a004953213ab00226a46b671218415e2..5bb9afa1fe264210d902f3271416f9932ed6929f 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -8888,6 +8888,7 @@ CREATE TABLE ci_runners (
     creator_id bigint,
     creation_state smallint DEFAULT 0 NOT NULL,
     allowed_plan_ids bigint[] DEFAULT '{}'::bigint[] NOT NULL,
+    sharding_key_id bigint,
     CONSTRAINT check_46c685e76f CHECK ((char_length((description)::text) <= 1024)),
     CONSTRAINT check_91230910ec CHECK ((char_length((name)::text) <= 256)),
     CONSTRAINT check_ce275cee06 CHECK ((char_length(maintainer_note) <= 1024))
diff --git a/lib/gitlab/background_migration/backfill_sharding_key_id_on_ci_runners.rb b/lib/gitlab/background_migration/backfill_sharding_key_id_on_ci_runners.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1e6552d895eb9550605c4d655dcd7e5296abb004
--- /dev/null
+++ b/lib/gitlab/background_migration/backfill_sharding_key_id_on_ci_runners.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module BackgroundMigration
+    class BackfillShardingKeyIdOnCiRunners < BatchedMigrationJob
+      operation_name :backfill_sharding_key_on_ci_runners
+      scope_to ->(relation) { relation.where(runner_type: [2, 3]) }
+
+      feature_category :runner
+
+      UPDATE_GROUP_RUNNERS_SQL = <<~SQL
+        UPDATE ci_runners
+        SET sharding_key_id = ci_runner_namespaces.namespace_id
+        FROM ci_runner_namespaces
+        WHERE ci_runners.id = ci_runner_namespaces.runner_id
+          AND ci_runners.id IN (?);
+      SQL
+
+      UPDATE_PROJECT_RUNNERS_SQL = <<~SQL
+        UPDATE ci_runners
+        SET sharding_key_id = (
+          SELECT ci_runner_projects.project_id
+          FROM ci_runner_projects
+          WHERE ci_runner_projects.runner_id = ci_runners.id
+          ORDER BY ci_runner_projects.id ASC
+          LIMIT 1
+        )
+        FROM ci_runner_projects
+        WHERE ci_runners.id = ci_runner_projects.runner_id
+          AND ci_runners.id IN (?);
+      SQL
+
+      class CiRunner < ::Ci::ApplicationRecord
+        self.table_name = 'ci_runners'
+      end
+
+      def perform
+        each_sub_batch do |sub_batch|
+          sub_batch = sub_batch.where(sharding_key_id: nil).limit(sub_batch_size).select(:id)
+
+          connection.exec_update(CiRunner.sanitize_sql([UPDATE_GROUP_RUNNERS_SQL, sub_batch.where(runner_type: 2)]))
+          connection.exec_update(CiRunner.sanitize_sql([UPDATE_PROJECT_RUNNERS_SQL, sub_batch.where(runner_type: 3)]))
+        end
+      end
+    end
+  end
+end
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb
index a35647a60c0a89eb7cdeea4420f83cac16eb3af5..03d652803403898f48cf52098ef04c15f7b9cc09 100644
--- a/spec/db/schema_spec.rb
+++ b/spec/db/schema_spec.rb
@@ -91,6 +91,7 @@
     ci_unit_test_failures: %w[project_id],
     p_ci_pipelines: %w[partition_id auto_canceled_by_partition_id auto_canceled_by_id],
     p_ci_runner_machine_builds: %w[project_id],
+    ci_runners: %w[sharding_key_id], # This value is meant to populate the partitioned table, no other usage
     ci_runner_projects: %w[runner_id],
     ci_sources_pipelines: %w[partition_id source_partition_id source_job_id],
     ci_sources_projects: %w[partition_id],
diff --git a/spec/factories/ci/runner_namespaces.rb b/spec/factories/ci/runner_namespaces.rb
index 3ae30e68ef166c20463e5a8969ac6c8e90fd40e3..3e37adeda506642698958da2d5aa9b67b1ee5313 100644
--- a/spec/factories/ci/runner_namespaces.rb
+++ b/spec/factories/ci/runner_namespaces.rb
@@ -6,8 +6,10 @@
 
     after(:build) do |runner_namespace, evaluator|
       if runner_namespace.runner.nil?
-        runner_namespace.runner = build(:ci_runner, :group, runner_namespaces: [runner_namespace])
         runner_namespace.namespace = evaluator.group
+        runner_namespace.runner =
+          build(:ci_runner, :group, runner_namespaces: [runner_namespace],
+            sharding_key_id: runner_namespace.namespace_id)
       end
     end
   end
diff --git a/spec/factories/ci/runner_projects.rb b/spec/factories/ci/runner_projects.rb
index 35184a416b158140028ff0fcbbd38b438e40e9ae..f1e04f92fc1e2e4f4d7a0730833a088e2ff5f36c 100644
--- a/spec/factories/ci/runner_projects.rb
+++ b/spec/factories/ci/runner_projects.rb
@@ -6,8 +6,9 @@
 
     after(:build) do |runner_project, evaluator|
       if runner_project.runner.nil?
-        runner_project.runner = build(:ci_runner, :project, runner_projects: [runner_project])
         runner_project.project = evaluator.project
+        runner_project.runner =
+          build(:ci_runner, :project, runner_projects: [runner_project], sharding_key_id: evaluator.project.id)
       end
     end
   end
diff --git a/spec/factories/ci/runners.rb b/spec/factories/ci/runners.rb
index 5389ead95a7964ea96cd75c4967879424128a1c3..3241f55a065d6abc45734b074ddf3750b13c5dca 100644
--- a/spec/factories/ci/runners.rb
+++ b/spec/factories/ci/runners.rb
@@ -20,10 +20,12 @@
     end
 
     after(:build) do |runner, evaluator|
+      runner.sharding_key_id ||= evaluator.projects.first&.id if runner.project_type?
       evaluator.projects.each do |proj|
         runner.runner_projects << build(:ci_runner_project, runner: runner, project: proj)
       end
 
+      runner.sharding_key_id ||= evaluator.groups.first&.id if runner.group_type?
       evaluator.groups.each do |group|
         runner.runner_namespaces << build(:ci_runner_namespace, runner: runner, namespace: group)
       end
@@ -107,6 +109,15 @@
       transient do
         without_projects { true }
       end
+
+      after(:build) do |runner, evaluator|
+        next if runner.sharding_key_id
+
+        # Point to a "no longer existing" project ID, as a project runner must always have been created
+        # with a sharding key id
+        runner.sharding_key_id = (2**63) - 1
+      end
+
       after(:create) do |runner, evaluator|
         runner.runner_projects.delete_all
       end
diff --git a/spec/lib/gitlab/background_migration/backfill_sharding_key_id_on_ci_runners_spec.rb b/spec/lib/gitlab/background_migration/backfill_sharding_key_id_on_ci_runners_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e4b44d7bf175a11d53826ce6afbbadf070ecf4c9
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_sharding_key_id_on_ci_runners_spec.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::BackfillShardingKeyIdOnCiRunners, schema: 20240923132401,
+  migration: :gitlab_ci, feature_category: :runner do
+  let(:runners) { table(:ci_runners) }
+  let(:runner_projects) { table(:ci_runner_projects) }
+  let(:runner_namespaces) { table(:ci_runner_namespaces) }
+  let(:connection) { Ci::ApplicationRecord.connection }
+
+  let(:args) do
+    min, max = runners.pick('MIN(id)', 'MAX(id)')
+
+    {
+      start_id: min,
+      end_id: max,
+      batch_table: 'ci_runners',
+      batch_column: 'id',
+      sub_batch_size: 100,
+      pause_ms: 0,
+      connection: connection
+    }
+  end
+
+  let(:group_id) { 100 }
+  let(:other_group_id) { 101 }
+  let(:project_id) { 1000 }
+  let(:other_project_id) { 1001 }
+  let!(:orphaned_project_runner) { create_project_runner(project_ids: []) }
+
+  subject(:perform_migration) { described_class.new(**args).perform }
+
+  before do
+    runners.create!(runner_type: 1)
+
+    create_project_runner(project_ids: project_id)
+    create_project_runner(project_ids: other_project_id)
+
+    create_group_runner(group_id: group_id)
+    create_group_runner(group_id: other_group_id)
+  end
+
+  it 'backfills sharding_key_id', :aggregate_failures do
+    expect { perform_migration }.not_to change { orphaned_project_runner.sharding_key_id }
+
+    runners.where(runner_type: 1).find_each do |runner|
+      expect(runner.sharding_key_id).to be_nil
+    end
+
+    runner_namespaces.find_each do |runner_namespace|
+      expect(runners.find(runner_namespace.runner_id).sharding_key_id).to eq(runner_namespace.namespace_id)
+    end
+
+    runner_projects.find_each do |runner_project|
+      expect(runners.find(runner_project.runner_id).sharding_key_id).to eq(runner_project.project_id)
+    end
+  end
+
+  context 'when a project runner is shared with other projects' do
+    let(:runner_project_ids) { [other_project_id, project_id] }
+
+    it 'backfills sharding_key_id with the id of the owner project' do
+      shared_project_runner = create_project_runner(project_ids: runner_project_ids)
+
+      expect do
+        perform_migration
+      end.to change { shared_project_runner.reload.sharding_key_id }.from(nil).to(other_project_id)
+    end
+
+    context 'when the project has a different owner project' do
+      let(:runner_project_ids) { [project_id, other_project_id] }
+
+      it 'backfills sharding_key_id with the id of the owner project' do
+        shared_project_runner = create_project_runner(project_ids: runner_project_ids)
+
+        expect do
+          perform_migration
+        end.to change { shared_project_runner.reload.sharding_key_id }.from(nil).to(project_id)
+      end
+    end
+  end
+
+  def create_group_runner(group_id:)
+    runners.create!(runner_type: 2, sharding_key_id: nil).tap do |runner|
+      runner_namespaces.create!(runner_id: runner.id, namespace_id: group_id)
+    end
+  end
+
+  def create_project_runner(project_ids:)
+    project_ids = Array.wrap(project_ids)
+
+    runners.create!(runner_type: 3, sharding_key_id: nil).tap do |runner|
+      project_ids.each do |project_id|
+        runner_projects.create!(runner_id: runner.id, project_id: project_id)
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/database/query_analyzers/prevent_cross_database_modification_spec.rb b/spec/lib/gitlab/database/query_analyzers/prevent_cross_database_modification_spec.rb
index 80c098cea101d00f3d217bdbf42557dcfe42499c..729061e3a0476ec70f865cd82ebb745775717876 100644
--- a/spec/lib/gitlab/database/query_analyzers/prevent_cross_database_modification_spec.rb
+++ b/spec/lib/gitlab/database/query_analyzers/prevent_cross_database_modification_spec.rb
@@ -181,7 +181,7 @@ def run_queries
           sql_log_contains: [/UPDATE "projects"/, /SELECT "ci_pipelines"\.\* FROM "ci_pipelines" .*FOR UPDATE/]
 
         context 'when the modification is inside a factory save! call' do
-          let(:runner) { create(:ci_runner, :project, projects: [build(:project)]) }
+          let(:runner) { create(:ci_runner, :project, projects: [create(:project)]) }
 
           it 'does not raise an error' do
             runner
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_group_type_active_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_group_type_active_metric_spec.rb
index 33605783671a322ebc41b9ea17b648ffccd11d73..c1313961202bbf9830d7a68d0c3232796b036c4c 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_group_type_active_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_group_type_active_metric_spec.rb
@@ -7,10 +7,7 @@
   let(:expected_value) { 1 }
 
   before do
-    create(:ci_runner,
-      :group,
-      groups: [group]
-    )
+    create(:ci_runner, :group, groups: [group])
   end
 
   it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' }
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_group_type_active_online_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_group_type_active_online_metric_spec.rb
index 24d6ea6f1e9c6165fe5338edb1d2188d1ddeb21d..37d31e325817734cc0ee405bb2e31fc8f97de3a2 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_group_type_active_online_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_group_type_active_online_metric_spec.rb
@@ -3,15 +3,11 @@
 require 'spec_helper'
 
 RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountCiRunnersGroupTypeActiveOnlineMetric, feature_category: :runner do
-  let(:group) { create(:group) }
+  let_it_be(:group) { create(:group) }
   let(:expected_value) { 1 }
 
   before do
-    create(:ci_runner,
-      :group,
-      groups: [group],
-      contacted_at: 1.second.ago
-    )
+    create(:ci_runner, :group, groups: [group], contacted_at: 1.second.ago)
   end
 
   it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' }
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_project_type_active_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_project_type_active_metric_spec.rb
index eeb699c1377ac4c0a4eccdbdd0521ee1ccceba91..811dcfccc0e20ec62aefd360c83577a10a8fbc4b 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_project_type_active_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_project_type_active_metric_spec.rb
@@ -3,14 +3,12 @@
 require 'spec_helper'
 
 RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountCiRunnersProjectTypeActiveMetric, feature_category: :runner do
-  let(:project) { build(:project) }
+  let_it_be(:project) { create(:project) }
+
   let(:expected_value) { 1 }
 
   before do
-    create(:ci_runner,
-      :project,
-      projects: [project]
-    )
+    create(:ci_runner, :project, projects: [project])
   end
 
   it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' }
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_project_type_active_online_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_project_type_active_online_metric_spec.rb
index c3ed752ae0487d048bb98ec53d1bc19c9ae5c624..ecc5b46582e600bf0b76db473b4838a1dbbe6652 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_project_type_active_online_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_runners_project_type_active_online_metric_spec.rb
@@ -3,15 +3,12 @@
 require 'spec_helper'
 
 RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountCiRunnersProjectTypeActiveOnlineMetric, feature_category: :runner do
-  let(:project) { build(:project) }
+  let_it_be(:project) { create(:project) }
+
   let(:expected_value) { 1 }
 
   before do
-    create(:ci_runner,
-      :project,
-      projects: [project],
-      contacted_at: 1.second.ago
-    )
+    create(:ci_runner, :project, projects: [project], contacted_at: 1.second.ago)
   end
 
   it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' }
diff --git a/spec/migrations/20240531202753_queue_backfill_or_drop_ci_pipeline_on_project_id_spec.rb b/spec/migrations/20240531202753_queue_backfill_or_drop_ci_pipeline_on_project_id_spec.rb
index e4fe7b1f9726340edb0e6d4cb6d6ba247438ec88..ad02215335ccb62abd3590fa4a58128985712adc 100644
--- a/spec/migrations/20240531202753_queue_backfill_or_drop_ci_pipeline_on_project_id_spec.rb
+++ b/spec/migrations/20240531202753_queue_backfill_or_drop_ci_pipeline_on_project_id_spec.rb
@@ -9,7 +9,7 @@
   it 'schedules a new batched migration' do
     reversible_migration do |migration|
       migration.before -> {
-        expect(batched_migration).not_to have_scheduled_batched_migration
+        expect(batched_migration).not_to have_scheduled_batched_migration(gitlab_schema: :gitlab_ci)
       }
 
       migration.after -> {
diff --git a/spec/migrations/20240821075507_queue_backfill_ci_job_variables_project_id_spec.rb b/spec/migrations/20240821075507_queue_backfill_ci_job_variables_project_id_spec.rb
index 3bf06909168de0c93821f180aa8ebdb158fb4568..d081e42b8af71e662322b883d0742aa82628d039 100644
--- a/spec/migrations/20240821075507_queue_backfill_ci_job_variables_project_id_spec.rb
+++ b/spec/migrations/20240821075507_queue_backfill_ci_job_variables_project_id_spec.rb
@@ -9,7 +9,7 @@
   it 'schedules a new batched migration' do
     reversible_migration do |migration|
       migration.before -> {
-        expect(batched_migration).not_to have_scheduled_batched_migration
+        expect(batched_migration).not_to have_scheduled_batched_migration(gitlab_schema: :gitlab_ci)
       }
 
       migration.after -> {
diff --git a/spec/migrations/20240923132401_queue_backfill_sharding_key_id_on_ci_runners_spec.rb b/spec/migrations/20240923132401_queue_backfill_sharding_key_id_on_ci_runners_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..bd35d1569c8a1d5aa1cc0093301a556ecbf63005
--- /dev/null
+++ b/spec/migrations/20240923132401_queue_backfill_sharding_key_id_on_ci_runners_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe QueueBackfillShardingKeyIdOnCiRunners, migration: :gitlab_ci, feature_category: :runner do
+  let!(:batched_migration) { described_class::MIGRATION }
+
+  it 'schedules a new batched migration' do
+    reversible_migration do |migration|
+      migration.before -> {
+        expect(batched_migration).not_to have_scheduled_batched_migration(gitlab_schema: :gitlab_ci)
+      }
+
+      migration.after -> {
+        expect(batched_migration).to have_scheduled_batched_migration(
+          gitlab_schema: :gitlab_ci,
+          table_name: :ci_runners,
+          column_name: :id,
+          interval: described_class::DELAY_INTERVAL,
+          batch_size: described_class::BATCH_SIZE,
+          sub_batch_size: described_class::SUB_BATCH_SIZE
+        )
+      }
+    end
+  end
+end
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index 3ebbbcfea32585251c5f1657422ebff441fa7f71..07ebc84da758e69f0ebea758f4b28d94b49eca0a 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -101,29 +101,89 @@
     it { is_expected.to validate_presence_of(:access_level) }
     it { is_expected.to validate_presence_of(:runner_type) }
     it { is_expected.to validate_presence_of(:registration_type) }
+    it { is_expected.to validate_presence_of(:sharding_key_id).on(:create) }
+
+    context 'when runner is instance type' do
+      let(:runner) { build(:ci_runner, :instance_type) }
+
+      it { expect(runner).to be_valid }
+
+      context 'when sharding_key_id is present' do
+        let(:runner) { build(:ci_runner, :instance_type, sharding_key_id: non_existing_record_id) }
+
+        it 'is invalid' do
+          expect(runner).to be_invalid
+          expect(runner.errors.full_messages).to contain_exactly('Runner cannot have sharding_key_id assigned')
+        end
+      end
+    end
 
     context 'when runner is not allowed to pick untagged jobs' do
       context 'when runner does not have tags' do
         let(:runner) { build(:ci_runner, tag_list: [], run_untagged: false) }
 
-        it 'is not valid' do
-          expect(runner).to be_invalid
-        end
+        it { expect(runner).to be_invalid }
       end
 
       context 'when runner has too many tags' do
         let(:runner) { build(:ci_runner, tag_list: (1..::Ci::Runner::TAG_LIST_MAX_LENGTH + 1).map { |i| "tag#{i}" }, run_untagged: false) }
 
-        it 'is not valid' do
-          expect(runner).to be_invalid
-        end
+        it { expect(runner).to be_invalid }
       end
 
       context 'when runner has tags' do
         let(:runner) { build(:ci_runner, tag_list: ['tag'], run_untagged: false) }
 
-        it 'is valid' do
-          expect(runner).to be_valid
+        it { expect(runner).to be_valid }
+      end
+    end
+
+    context 'shading_key_id validations' do
+      context 'with instance runner' do
+        let(:runner) { build(:ci_runner, :instance) }
+
+        it { expect(runner).to be_valid }
+
+        context 'when sharding_key_id is not present' do
+          before do
+            runner.sharding_key_id = nil
+          end
+
+          it { expect(runner).to be_valid }
+        end
+      end
+
+      context 'with group runner' do
+        let(:runner) { build(:ci_runner, :group, groups: [group]) }
+
+        it { expect(runner).to be_valid }
+
+        context 'when sharding_key_id is not present' do
+          before do
+            runner.sharding_key_id = nil
+          end
+
+          it 'adds error to model', :aggregate_failures do
+            expect(runner).not_to be_valid
+            expect(runner.errors[:sharding_key_id]).to contain_exactly("can't be blank")
+          end
+        end
+      end
+
+      context 'with project runner' do
+        let(:runner) { build(:ci_runner, :project, projects: [project]) }
+
+        it { expect(runner).to be_valid }
+
+        context 'when sharding_key_id is not present' do
+          before do
+            runner.sharding_key_id = nil
+          end
+
+          it 'adds error to model', :aggregate_failures do
+            expect(runner).not_to be_valid
+            expect(runner.errors[:sharding_key_id]).to contain_exactly("can't be blank")
+          end
         end
       end
     end
@@ -490,6 +550,10 @@
         expect(runner).to be_project_type
         expect(runner.runner_projects.pluck(:project_id)).to contain_exactly(project.id, other_project.id)
       end
+
+      it 'does not change sharding_key_id' do
+        expect { assign_to }.not_to change { runner.sharding_key_id }.from(other_project.id)
+      end
     end
   end
 
diff --git a/spec/services/ci/runners/create_runner_service_spec.rb b/spec/services/ci/runners/create_runner_service_spec.rb
index efab371482b0789a6d8cc6d4fae91c5e18a2231c..1845ca78f786cb434fe0a8385f186ddf56d574a0 100644
--- a/spec/services/ci/runners/create_runner_service_spec.rb
+++ b/spec/services/ci/runners/create_runner_service_spec.rb
@@ -220,6 +220,10 @@
 
       it_behaves_like 'it can create a runner'
 
+      it 'populates sharding_key_id correctly' do
+        expect(runner.sharding_key_id).to eq(group.id)
+      end
+
       context 'with missing scope param' do
         let(:params) { { runner_type: 'group_type' } }
 
@@ -261,6 +265,10 @@
 
       it_behaves_like 'it can create a runner'
 
+      it 'populates sharding_key_id correctly' do
+        expect(runner.sharding_key_id).to eq(project.id)
+      end
+
       context 'with missing scope param' do
         let(:params) { { runner_type: 'project_type' } }