diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 5b1a25396afa894290150a4921460656c0a5d19a..7e186baf13401f9c3d7f3dbb3186c1d38b23f829 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -282,6 +282,15 @@
   :weight: 1
   :idempotent: true
   :tags: []
+- :name: cronjob:container_registry_migration_observer
+  :worker_name: ContainerRegistry::Migration::ObserverWorker
+  :feature_category: :container_registry
+  :has_external_dependencies:
+  :urgency: :low
+  :resource_boundary: :unknown
+  :weight: 1
+  :idempotent: true
+  :tags: []
 - :name: cronjob:database_batched_background_migration
   :worker_name: Database::BatchedBackgroundMigrationWorker
   :feature_category: :database
diff --git a/app/workers/container_registry/migration/observer_worker.rb b/app/workers/container_registry/migration/observer_worker.rb
new file mode 100644
index 0000000000000000000000000000000000000000..757c4fd11a5e308cbc5efaae54fc8e46f7f46440
--- /dev/null
+++ b/app/workers/container_registry/migration/observer_worker.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module ContainerRegistry
+  module Migration
+    class ObserverWorker
+      include ApplicationWorker
+      # This worker does not perform work scoped to a context
+      include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
+
+      COUNT_BATCH_SIZE = 50000
+
+      data_consistency :sticky
+      feature_category :container_registry
+      urgency :low
+      deduplicate :until_executed, including_scheduled: true
+      idempotent!
+
+      def perform
+        return unless ::ContainerRegistry::Migration.enabled?
+
+        use_replica_if_available do
+          ContainerRepository::MIGRATION_STATES.each do |state|
+            relation = ContainerRepository.with_migration_state(state)
+            count = ::Gitlab::Database::BatchCount.batch_count(
+              relation, batch_size: COUNT_BATCH_SIZE
+            )
+            name = "#{state}_count".to_sym
+            log_extra_metadata_on_done(name, count)
+          end
+        end
+      end
+
+      private
+
+      def use_replica_if_available(&block)
+        ::Gitlab::Database::LoadBalancing::Session.current.use_replicas_for_read_queries(&block)
+      end
+    end
+  end
+end
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 692488226580e5517786a9c119c95bf065758438..c634dbafe836d1060a5baa1aac93259f223d08f4 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -542,6 +542,9 @@
 Settings.cron_jobs['container_registry_migration_guard_worker'] ||= Settingslogic.new({})
 Settings.cron_jobs['container_registry_migration_guard_worker']['cron'] ||= '*/10 * * * *'
 Settings.cron_jobs['container_registry_migration_guard_worker']['job_class'] = 'ContainerRegistry::Migration::GuardWorker'
+Settings.cron_jobs['container_registry_migration_observer_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['container_registry_migration_observer_worker']['cron'] ||= '*/30 * * * *'
+Settings.cron_jobs['container_registry_migration_observer_worker']['job_class'] = 'ContainerRegistry::Migration::ObserverWorker'
 Settings.cron_jobs['image_ttl_group_policy_worker'] ||= Settingslogic.new({})
 Settings.cron_jobs['image_ttl_group_policy_worker']['cron'] ||= '40 0 * * *'
 Settings.cron_jobs['image_ttl_group_policy_worker']['job_class'] = 'DependencyProxy::ImageTtlGroupPolicyWorker'
diff --git a/db/migrate/20220202034409_add_tmp_index_on_id_and_migration_state_to_container_repositories.rb b/db/migrate/20220202034409_add_tmp_index_on_id_and_migration_state_to_container_repositories.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b999c871a3ef7875b046fb72bbc072e5ec6b184e
--- /dev/null
+++ b/db/migrate/20220202034409_add_tmp_index_on_id_and_migration_state_to_container_repositories.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class AddTmpIndexOnIdAndMigrationStateToContainerRepositories < Gitlab::Database::Migration[1.0]
+  INDEX_NAME = 'tmp_index_container_repositories_on_id_migration_state'
+
+  disable_ddl_transaction!
+
+  # Temporary index to be removed https://gitlab.com/gitlab-org/gitlab/-/issues/351783
+  def up
+    add_concurrent_index :container_repositories, [:id, :migration_state], name: INDEX_NAME
+  end
+
+  def down
+    remove_concurrent_index_by_name :container_repositories, INDEX_NAME
+  end
+end
diff --git a/db/schema_migrations/20220202034409 b/db/schema_migrations/20220202034409
new file mode 100644
index 0000000000000000000000000000000000000000..4eb359f45e6d3c16130fed325d8538cb2259a451
--- /dev/null
+++ b/db/schema_migrations/20220202034409
@@ -0,0 +1 @@
+0efe482aa626cf80912feaa1176837253b094fc434f273bee35b5fe3e8ce4243
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 8d3a7607a1f7828aefe7a6cd4be80c620534c75d..c1645c2169f55e9726be7c03ac51d8fe3079bbc8 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -28320,6 +28320,8 @@ CREATE INDEX tmp_idx_deduplicate_vulnerability_occurrences ON vulnerability_occu
 
 CREATE INDEX tmp_idx_vulnerability_occurrences_on_id_where_report_type_7_99 ON vulnerability_occurrences USING btree (id) WHERE (report_type = ANY (ARRAY[7, 99]));
 
+CREATE INDEX tmp_index_container_repositories_on_id_migration_state ON container_repositories USING btree (id, migration_state);
+
 CREATE INDEX tmp_index_for_namespace_id_migration_on_routes ON routes USING btree (id) WHERE ((namespace_id IS NULL) AND ((source_type)::text = 'Namespace'::text));
 
 CREATE INDEX tmp_index_members_on_state ON members USING btree (state) WHERE (state = 2);
diff --git a/spec/workers/container_registry/migration/observer_worker_spec.rb b/spec/workers/container_registry/migration/observer_worker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fec6640d7ec88478682bf38e25dae8d333b8ba75
--- /dev/null
+++ b/spec/workers/container_registry/migration/observer_worker_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ContainerRegistry::Migration::ObserverWorker, :aggregate_failures do
+  let(:worker) { described_class.new }
+
+  describe '#perform' do
+    subject { worker.perform }
+
+    context 'when the migration feature flag is disabled' do
+      before do
+        stub_feature_flags(container_registry_migration_phase2_enabled: false)
+      end
+
+      it 'does nothing' do
+        expect(worker).not_to receive(:log_extra_metadata_on_done)
+
+        subject
+      end
+    end
+
+    context 'when the migration is enabled' do
+      before do
+        create_list(:container_repository, 3)
+        create(:container_repository, :pre_importing)
+        create(:container_repository, :pre_import_done)
+        create_list(:container_repository, 2, :importing)
+        create(:container_repository, :import_aborted)
+        # batch_count is not allowed within a transaction but
+        # all rspec tests run inside of a transaction.
+        # This mocks the false positive.
+        allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false) # rubocop:disable Database/MultipleDatabases
+      end
+
+      it 'logs all the counts' do
+        expect(worker).to receive(:log_extra_metadata_on_done).with(:default_count, 3)
+        expect(worker).to receive(:log_extra_metadata_on_done).with(:pre_importing_count, 1)
+        expect(worker).to receive(:log_extra_metadata_on_done).with(:pre_import_done_count, 1)
+        expect(worker).to receive(:log_extra_metadata_on_done).with(:importing_count, 2)
+        expect(worker).to receive(:log_extra_metadata_on_done).with(:import_done_count, 0)
+        expect(worker).to receive(:log_extra_metadata_on_done).with(:import_aborted_count, 1)
+        expect(worker).to receive(:log_extra_metadata_on_done).with(:import_skipped_count, 0)
+
+        subject
+      end
+
+      context 'with load balancing enabled', :db_load_balancing do
+        it 'uses the replica' do
+          expect(Gitlab::Database::LoadBalancing::Session.current).to receive(:use_replicas_for_read_queries).and_call_original
+
+          subject
+        end
+      end
+    end
+  end
+end