diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index de8251411549cfd28301a58444570df1ad449207..a9d25a0a67906f1cd92c00d3b05a1ddb3f98c221 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -757,6 +757,8 @@
   - 1
 - - search_zoekt_initial_indexing_event
   - 1
+- - search_zoekt_lost_node_event
+  - 1
 - - search_zoekt_namespace_indexer
   - 1
 - - search_zoekt_namespace_initial_indexing
diff --git a/doc/integration/exact_code_search/zoekt.md b/doc/integration/exact_code_search/zoekt.md
index 8d8eb7f319ca2c5894b831219a9bbcdcd3b1d915..783afc75d446af90c0c4c7252b2d8bbe4d695e0c 100644
--- a/doc/integration/exact_code_search/zoekt.md
+++ b/doc/integration/exact_code_search/zoekt.md
@@ -56,6 +56,23 @@ To enable [exact code search](../../user/search/exact_code_search.md) in GitLab:
 1. Select the **Enable indexing for exact code search** and **Enable exact code search** checkboxes.
 1. Select **Save changes**.
 
+## Delete offline nodes automatically
+
+Prerequisites:
+
+- You must have administrator access to the instance.
+
+You can automatically delete Zoekt nodes that are offline for more than 12 hours
+and their related indices, repositories, and tasks.
+
+To delete offline nodes automatically:
+
+1. On the left sidebar, at the bottom, select **Admin**.
+1. Select **Settings > Search**.
+1. Expand **Exact code search configuration**.
+1. Select the **Delete offline nodes automatically after 12 hours** checkbox.
+1. Select **Save changes**.
+
 ## Index root namespaces automatically
 
 Prerequisites:
diff --git a/ee/app/events/search/zoekt/lost_node_event.rb b/ee/app/events/search/zoekt/lost_node_event.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0c96cafd11944e79c4a9ca6139173aaee783d8e0
--- /dev/null
+++ b/ee/app/events/search/zoekt/lost_node_event.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Search
+  module Zoekt
+    class LostNodeEvent < ::Gitlab::EventStore::Event
+      def schema
+        {
+          'type' => 'object',
+          'properties' => {
+            'zoekt_node_id' => { 'type' => 'integer' }
+          },
+          'required' => %w[zoekt_node_id]
+        }
+      end
+    end
+  end
+end
diff --git a/ee/app/helpers/ee/application_settings_helper.rb b/ee/app/helpers/ee/application_settings_helper.rb
index a1ef2515f422338f4d7056fc6f594a039453d983..c47bee24bc42c14908738b416a093cae96745fdb 100644
--- a/ee/app/helpers/ee/application_settings_helper.rb
+++ b/ee/app/helpers/ee/application_settings_helper.rb
@@ -76,6 +76,7 @@ def visible_attributes
         :duo_features_enabled,
         :lock_duo_features_enabled,
         :duo_availability,
+        :zoekt_auto_delete_lost_nodes,
         :zoekt_auto_index_root_namespace,
         :zoekt_cpu_to_tasks_ratio,
         :zoekt_indexing_enabled,
@@ -251,6 +252,12 @@ def sync_purl_types_checkboxes(form)
 
     def zoekt_settings_checkboxes(form)
       [
+        form.gitlab_ui_checkbox_component(
+          :zoekt_auto_delete_lost_nodes,
+          format(_("Delete offline nodes automatically after %{label}"),
+            label: ::Search::Zoekt::Node::LOST_DURATION_THRESHOLD.inspect),
+          checkbox_options: { checked: @application_setting.zoekt_auto_delete_lost_nodes, multiple: false }
+        ),
         form.gitlab_ui_checkbox_component(
           :zoekt_auto_index_root_namespace,
           _('Index root namespaces automatically'),
diff --git a/ee/app/models/ee/application_setting.rb b/ee/app/models/ee/application_setting.rb
index 0aa44fff2f21e0b2d7679fad94986f9427abd8a4..94fe028f986966e3e25aa19fff76ba4d46f4d818 100644
--- a/ee/app/models/ee/application_setting.rb
+++ b/ee/app/models/ee/application_setting.rb
@@ -21,6 +21,7 @@ module ApplicationSetting
         use_clickhouse_for_analytics: [:boolean, { default: false }]
 
       jsonb_accessor :zoekt_settings,
+        zoekt_auto_delete_lost_nodes: [:boolean, { default: true }],
         zoekt_indexing_enabled: [:boolean, { default: false }],
         zoekt_indexing_paused: [:boolean, { default: false }],
         zoekt_search_enabled: [:boolean, { default: false }],
diff --git a/ee/app/models/search/zoekt/node.rb b/ee/app/models/search/zoekt/node.rb
index ab7d4eb419ed5491993056254ed9605aae93545c..03139243991f6b0b33c0c06e93fedf85ac35065b 100644
--- a/ee/app/models/search/zoekt/node.rb
+++ b/ee/app/models/search/zoekt/node.rb
@@ -7,6 +7,8 @@ class Node < ApplicationRecord
 
       DEFAULT_CONCURRENCY_LIMIT = 20
       MAX_CONCURRENCY_LIMIT = 200
+      LOST_DURATION_THRESHOLD = 12.hours
+      ONLINE_DURATION_THRESHOLD = 1.minute
       WATERMARK_LIMIT_LOW = 0.6
       WATERMARK_LIMIT_HIGH = 0.7
       WATERMARK_LIMIT_CRITICAL = 0.85
@@ -29,7 +31,8 @@ class Node < ApplicationRecord
       attribute :metadata, :ind_jsonb # for indifferent access
 
       scope :by_name, ->(*names) { where("metadata->>'name' IN (?)", names) }
-      scope :online, -> { where(last_seen_at: 1.minute.ago..) }
+      scope :lost, -> { where(last_seen_at: ..LOST_DURATION_THRESHOLD.ago) }
+      scope :online, -> { where(last_seen_at: ONLINE_DURATION_THRESHOLD.ago..) }
 
       def self.find_or_initialize_by_task_request(params)
         params = params.with_indifferent_access
@@ -50,6 +53,15 @@ def self.find_or_initialize_by_task_request(params)
         end
       end
 
+      def self.marking_lost_enabled?
+        return false if Feature.disabled?(:zoekt_internal_api_register_nodes, Feature.current_request)
+        return false if Gitlab::CurrentSettings.zoekt_indexing_paused?
+        return false unless Gitlab::CurrentSettings.zoekt_indexing_enabled?
+        return false unless Gitlab::CurrentSettings.zoekt_auto_delete_lost_nodes?
+
+        true
+      end
+
       def concurrency_limit
         override = metadata['concurrency_override'].to_i
         return override if override > 0
@@ -95,6 +107,10 @@ def storage_percent_used
 
         used_bytes / total_bytes.to_f
       end
+
+      def lost?
+        last_seen_at <= LOST_DURATION_THRESHOLD.ago
+      end
     end
   end
 end
diff --git a/ee/app/models/search/zoekt/repository.rb b/ee/app/models/search/zoekt/repository.rb
index 4fb01a59b56e6066103a7424e3d5ae0c7bf6c537..ea1d65c6968b42b569d9de057e478c4275777c62 100644
--- a/ee/app/models/search/zoekt/repository.rb
+++ b/ee/app/models/search/zoekt/repository.rb
@@ -45,6 +45,8 @@ class Repository < ApplicationRecord
         where(state: [:orphaned, :pending_deletion])
       end
 
+      scope :for_zoekt_indices, ->(indices) { where(zoekt_index: indices) }
+
       def self.create_tasks(project_id:, zoekt_index:, task_type:, perform_at:)
         project = Project.find_by_id(project_id)
         find_or_initialize_by(project_identifier: project_id, project: project, zoekt_index: zoekt_index).tap do |item|
diff --git a/ee/app/services/search/zoekt/scheduling_service.rb b/ee/app/services/search/zoekt/scheduling_service.rb
index 098b847c645f497f0d6c16d8f9d224bacca365d2..4e806b461bc32b17b7d7722445c87c2a8218115e 100644
--- a/ee/app/services/search/zoekt/scheduling_service.rb
+++ b/ee/app/services/search/zoekt/scheduling_service.rb
@@ -6,19 +6,20 @@ class SchedulingService
       include Gitlab::Loggable
 
       TASKS = %i[
+        auto_index_self_managed
         dot_com_rollout
         eviction
-        remove_expired_subscriptions
-        node_assignment
-        mark_indices_as_ready
-        initial_indexing
-        auto_index_self_managed
-        update_replica_states
-        report_metrics
         index_should_be_marked_as_orphaned_check
         index_to_delete_check
+        initial_indexing
+        lost_nodes_check
+        mark_indices_as_ready
+        node_assignment
+        remove_expired_subscriptions
         repo_should_be_marked_as_orphaned_check
         repo_to_delete_check
+        report_metrics
+        update_replica_states
       ].freeze
 
       BUFFER_FACTOR = 3
@@ -379,6 +380,17 @@ def repo_to_delete_check
           end
         end
       end
+
+      def lost_nodes_check
+        return false if Rails.env.development?
+        return false unless Node.marking_lost_enabled?
+
+        execute_every 10.minutes, cache_key: :lost_nodes_check do
+          Node.lost.select(:id).find_each do |node|
+            Gitlab::EventStore.publish(Search::Zoekt::LostNodeEvent.new(data: { zoekt_node_id: node.id }))
+          end
+        end
+      end
     end
   end
 end
diff --git a/ee/app/validators/json_schemas/application_setting_zoekt_settings.json b/ee/app/validators/json_schemas/application_setting_zoekt_settings.json
index 5debded421c80a0cad8a7ceb998b63907e34d098..ef95b32d61eaf825bc3dac0c79ddf142b89035d4 100644
--- a/ee/app/validators/json_schemas/application_setting_zoekt_settings.json
+++ b/ee/app/validators/json_schemas/application_setting_zoekt_settings.json
@@ -4,6 +4,10 @@
   "type": "object",
   "additionalProperties": false,
   "properties": {
+    "zoekt_auto_delete_lost_nodes": {
+      "type": "boolean",
+      "description": "If lost nodes and related records should get automatically deleted"
+    },
     "zoekt_auto_index_root_namespace": {
       "type": "boolean",
       "description": "If all root namespaces need to be indexed automatically for the instance"
diff --git a/ee/app/workers/all_queues.yml b/ee/app/workers/all_queues.yml
index 4481806d6fac8517324cbc2f961575f50b8aa9b4..fce13ece967653af4cd1b50cfd28403ec8141c61 100644
--- a/ee/app/workers/all_queues.yml
+++ b/ee/app/workers/all_queues.yml
@@ -2208,6 +2208,15 @@
   :weight: 1
   :idempotent: true
   :tags: []
+- :name: search_zoekt_lost_node_event
+  :worker_name: Search::Zoekt::LostNodeEventWorker
+  :feature_category: :global_search
+  :has_external_dependencies: false
+  :urgency: :low
+  :resource_boundary: :unknown
+  :weight: 1
+  :idempotent: true
+  :tags: []
 - :name: search_zoekt_namespace_indexer
   :worker_name: Search::Zoekt::NamespaceIndexerWorker
   :feature_category: :global_search
diff --git a/ee/app/workers/search/zoekt/lost_node_event_worker.rb b/ee/app/workers/search/zoekt/lost_node_event_worker.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a1137373193bfb37dc26ce697a015528396e1013
--- /dev/null
+++ b/ee/app/workers/search/zoekt/lost_node_event_worker.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Search
+  module Zoekt
+    class LostNodeEventWorker
+      include Gitlab::EventStore::Subscriber
+      prepend ::Geo::SkipSecondary
+
+      feature_category :global_search
+      idempotent!
+      deduplicate :until_executed
+
+      BATCH_SIZE = 10_000
+
+      def handle_event(event)
+        return false unless Search::Zoekt::Node.marking_lost_enabled?
+
+        node = Node.find_by_id(event.data[:zoekt_node_id])
+        return unless node
+        return unless node.lost?
+
+        log_metadata = {}
+        start_time = Time.current
+        ApplicationRecord.transaction do
+          node.lock!
+          count = 0
+          node.tasks.each_batch(of: BATCH_SIZE) { |batch| count += batch.delete_all }
+          log_metadata[:deleted_tasks_count] = count
+          indices = node.indices
+          unless indices.empty?
+            count = 0
+            Repository.for_zoekt_indices(indices).each_batch(of: BATCH_SIZE) { |batch| count += batch.delete_all }
+            log_metadata[:deleted_repos_count] = count
+            count = 0
+            indices.each_batch(of: BATCH_SIZE) { |batch| count += batch.delete_all }
+            log_metadata[:deleted_indices_count] = count
+          end
+
+          node.delete
+        end
+        log_metadata[:transaction_time] = Time.current - start_time
+        log_hash_metadata_on_done(node_id: node.id, node_name: node[:metadata][:name], metadata: log_metadata)
+      end
+    end
+  end
+end
diff --git a/ee/lib/ee/gitlab/event_store.rb b/ee/lib/ee/gitlab/event_store.rb
index 1238077757902de53187303b37b178109440b139..53f3359397cf976408e3031396567a3574e01945 100644
--- a/ee/lib/ee/gitlab/event_store.rb
+++ b/ee/lib/ee/gitlab/event_store.rb
@@ -177,6 +177,9 @@ def subscribe_to_zoekt_events(store)
 
           store.subscribe ::Search::Zoekt::InitialIndexingEventWorker,
             to: ::Search::Zoekt::InitialIndexingEvent
+
+          store.subscribe ::Search::Zoekt::LostNodeEventWorker,
+            to: ::Search::Zoekt::LostNodeEvent
         end
       end
     end
diff --git a/ee/spec/factories/zoekt/nodes.rb b/ee/spec/factories/zoekt/nodes.rb
index c8dc6f69b6234527e08b9e1d49903d6315d7f1c4..3c2227bc37b3d7385b8deb9d8d6794bcc4ffbf1b 100644
--- a/ee/spec/factories/zoekt/nodes.rb
+++ b/ee/spec/factories/zoekt/nodes.rb
@@ -17,6 +17,14 @@
       total_bytes { 100_000_000 }
     end
 
+    trait :offline do
+      last_seen_at { (Search::Zoekt::Node::ONLINE_DURATION_THRESHOLD + 1.minute).ago }
+    end
+
+    trait :lost do
+      last_seen_at { (Search::Zoekt::Node::LOST_DURATION_THRESHOLD + 1.minute).ago }
+    end
+
     trait :not_enough_free_space do
       total_bytes { 100_000_000 }
       used_bytes { 90_000_000 }
diff --git a/ee/spec/helpers/ee/application_settings_helper_spec.rb b/ee/spec/helpers/ee/application_settings_helper_spec.rb
index e3126e9e7b905e8fbee12a23ad297eb40872e7ee..8269c3acc03e9a08f68b482fdc5d71974948a1b9 100644
--- a/ee/spec/helpers/ee/application_settings_helper_spec.rb
+++ b/ee/spec/helpers/ee/application_settings_helper_spec.rb
@@ -15,7 +15,7 @@
 
     it 'contains zoekt parameters' do
       expected_fields = %i[
-        zoekt_auto_index_root_namespace zoekt_indexing_enabled
+        zoekt_auto_delete_lost_nodes zoekt_auto_index_root_namespace zoekt_indexing_enabled
         zoekt_indexing_paused zoekt_search_enabled zoekt_cpu_to_tasks_ratio
       ]
       expect(visible_attributes).to include(*expected_fields)
@@ -203,6 +203,7 @@
     let_it_be(:application_setting) { build(:application_setting) }
 
     before do
+      application_setting.zoekt_auto_delete_lost_nodes = true
       application_setting.zoekt_auto_index_root_namespace = false
       application_setting.zoekt_indexing_enabled = true
       application_setting.zoekt_indexing_paused = false
@@ -213,10 +214,11 @@
     it 'returns correctly checked checkboxes' do
       helper.gitlab_ui_form_for(application_setting, url: advanced_search_admin_application_settings_path) do |form|
         result = helper.zoekt_settings_checkboxes(form)
-        expect(result[0]).not_to have_checked_field('Index all the namespaces', with: 1)
-        expect(result[1]).to have_checked_field('Enable indexing for exact code search', with: 1)
-        expect(result[2]).not_to have_checked_field('Pause indexing for exact code search', with: 1)
-        expect(result[3]).to have_checked_field('Enable exact code search', with: 1)
+        expect(result[0]).to have_checked_field("Delete offline nodes automatically after #{::Search::Zoekt::Node::LOST_DURATION_THRESHOLD.inspect}", with: 1)
+        expect(result[1]).not_to have_checked_field('Index all the namespaces', with: 1)
+        expect(result[2]).to have_checked_field('Enable indexing for exact code search', with: 1)
+        expect(result[3]).not_to have_checked_field('Pause indexing for exact code search', with: 1)
+        expect(result[4]).to have_checked_field('Enable exact code search', with: 1)
       end
     end
   end
diff --git a/ee/spec/lib/ee/gitlab/event_store_spec.rb b/ee/spec/lib/ee/gitlab/event_store_spec.rb
index 1dea806eb6cf6473c974e0760e19addebef20e1c..7ac073a451003920474029a8763cccc5a0018c23 100644
--- a/ee/spec/lib/ee/gitlab/event_store_spec.rb
+++ b/ee/spec/lib/ee/gitlab/event_store_spec.rb
@@ -52,6 +52,7 @@
         Search::Zoekt::OrphanedRepoEvent,
         Search::Zoekt::RepoMarkedAsToDeleteEvent,
         Search::Zoekt::TaskFailedEvent,
+        Search::Zoekt::LostNodeEvent,
         Security::PolicyDeletedEvent,
         ::Members::MembershipModifiedByAdminEvent
       ])
diff --git a/ee/spec/models/application_setting_spec.rb b/ee/spec/models/application_setting_spec.rb
index f76c07e1bc29051e570990b55d726827ad05bb27..287f2f62e7898b82412bfcf03a8dec5ebea75c00 100644
--- a/ee/spec/models/application_setting_spec.rb
+++ b/ee/spec/models/application_setting_spec.rb
@@ -24,6 +24,7 @@
     it { expect(setting.receptive_cluster_agents_enabled).to eq(false) }
     it { expect(setting.security_approval_policies_limit).to eq(5) }
     it { expect(setting.use_clickhouse_for_analytics).to eq(false) }
+    it { expect(setting.zoekt_auto_delete_lost_nodes).to eq(true) }
     it { expect(setting.zoekt_auto_index_root_namespace).to eq(false) }
     it { expect(setting.zoekt_cpu_to_tasks_ratio).to eq(1.0) }
     it { expect(setting.zoekt_indexing_enabled).to eq(false) }
diff --git a/ee/spec/models/search/zoekt/node_spec.rb b/ee/spec/models/search/zoekt/node_spec.rb
index 1a4e09ceff3e8c938412949b1827725db554c706..7b75d18241b4fd7ab271c7b0430ec8bc2c12d47e 100644
--- a/ee/spec/models/search/zoekt/node_spec.rb
+++ b/ee/spec/models/search/zoekt/node_spec.rb
@@ -24,9 +24,18 @@
   end
 
   describe 'scopes' do
-    describe '.online' do
-      let_it_be(:online_node) { create(:zoekt_node, last_seen_at: 1.second.ago) }
+    describe '.lost', :freeze_time do
       let_it_be(:offline_node) { create(:zoekt_node, last_seen_at: 10.minutes.ago) }
+      let_it_be(:lost_node) { create(:zoekt_node, :lost) }
+
+      it 'returns all the lost nodes' do
+        expect(described_class.lost).to contain_exactly(lost_node)
+      end
+    end
+
+    describe '.online', :freeze_time do
+      let_it_be(:online_node) { create(:zoekt_node) }
+      let_it_be(:offline_node) { create(:zoekt_node, :offline) }
 
       it 'returns nodes considered to be online' do
         expect(described_class.online).to contain_exactly(node, online_node)
@@ -130,6 +139,52 @@
     end
   end
 
+  describe '.marking_lost_enabled?', :zoekt_settings_enabled do
+    it 'returns true' do
+      expect(described_class.marking_lost_enabled?).to eq true
+    end
+
+    context 'when FF zoekt_internal_api_register_nodes is disabled' do
+      before do
+        stub_feature_flags(zoekt_internal_api_register_nodes: false)
+      end
+
+      it 'returns false' do
+        expect(described_class.marking_lost_enabled?).to eq false
+      end
+    end
+
+    context 'when application setting zoekt_indexing_paused? is enabled' do
+      before do
+        stub_ee_application_setting(zoekt_indexing_paused: true)
+      end
+
+      it 'returns false' do
+        expect(described_class.marking_lost_enabled?).to eq false
+      end
+    end
+
+    context 'when application setting zoekt_indexing_enabled? is disabled' do
+      before do
+        stub_ee_application_setting(zoekt_indexing_enabled: false)
+      end
+
+      it 'returns false' do
+        expect(described_class.marking_lost_enabled?).to eq false
+      end
+    end
+
+    context 'when application setting zoekt_auto_delete_lost_nodes? is disabled' do
+      before do
+        stub_ee_application_setting(zoekt_auto_delete_lost_nodes: false)
+      end
+
+      it 'returns false' do
+        expect(described_class.marking_lost_enabled?).to eq false
+      end
+    end
+  end
+
   describe '#backoff' do
     it 'returns a NodeBackoff' do
       expect(::Search::Zoekt::NodeBackoff).to receive(:new).with(node).and_return(:backoff)
diff --git a/ee/spec/models/search/zoekt/repository_spec.rb b/ee/spec/models/search/zoekt/repository_spec.rb
index 191919f3001c9af96471ab8c270052c80dd2001f..61966dde540511b4da739223eddf6b852f77d980 100644
--- a/ee/spec/models/search/zoekt/repository_spec.rb
+++ b/ee/spec/models/search/zoekt/repository_spec.rb
@@ -45,6 +45,21 @@
         expect(described_class.non_ready).to contain_exactly zoekt_repository
       end
     end
+
+    describe '.for_zoekt_indices' do
+      let_it_be(:zoekt_index) { create(:zoekt_index) }
+      let_it_be(:zoekt_index2) { create(:zoekt_index) }
+      let_it_be(:zoekt_index3) { create(:zoekt_index) }
+      let_it_be(:zoekt_repository) { create(:zoekt_repository, zoekt_index: zoekt_index) }
+      let_it_be(:zoekt_repository2) { create(:zoekt_repository, zoekt_index: zoekt_index) }
+      let_it_be(:zoekt_repository3) { create(:zoekt_repository, zoekt_index: zoekt_index2) }
+
+      it 'returns records for matching zoekt indices' do
+        create(:zoekt_repository, zoekt_index: zoekt_index3)
+        expect(described_class.for_zoekt_indices([zoekt_index, zoekt_index2])).to contain_exactly zoekt_repository,
+          zoekt_repository2, zoekt_repository3
+      end
+    end
   end
 
   describe '.create_tasks', :freeze_time do
diff --git a/ee/spec/services/search/zoekt/scheduling_service_spec.rb b/ee/spec/services/search/zoekt/scheduling_service_spec.rb
index ef2c3abf04dfc9ae0d036c37d8f0b663a894a1a3..44170e41083c86eaaab5db896c2fdff6c8e78a0a 100644
--- a/ee/spec/services/search/zoekt/scheduling_service_spec.rb
+++ b/ee/spec/services/search/zoekt/scheduling_service_spec.rb
@@ -636,4 +636,57 @@
         .with(expected_data)
     end
   end
+
+  describe '#lost_nodes_check', :zoekt_settings_enabled do
+    let(:task) { :lost_nodes_check }
+    let_it_be_with_reload(:lost_node) { create(:zoekt_node, :lost) }
+
+    before do
+      create(:zoekt_node)
+    end
+
+    it 'publishes LostNodeEvent' do
+      expect { execute_task }.to publish_event(Search::Zoekt::LostNodeEvent).with({ zoekt_node_id: lost_node.id })
+    end
+
+    context 'when there are no lost nodes' do
+      before do
+        Search::Zoekt::Node.update_all(last_seen_at: Time.current)
+      end
+
+      it 'does not publish LostNodeEvent' do
+        expect { execute_task }.to not_publish_event(Search::Zoekt::LostNodeEvent)
+      end
+    end
+
+    context 'when on development environment' do
+      before do
+        allow(Rails.env).to receive(:development?).and_return(true)
+      end
+
+      it 'does not publish LostNodeEvent' do
+        expect { execute_task }.to not_publish_event(Search::Zoekt::LostNodeEvent)
+      end
+    end
+
+    context 'when marking_lost_enabled? is false' do
+      before do
+        allow(Search::Zoekt::Node).to receive(:marking_lost_enabled?).and_return false
+      end
+
+      it 'does not publish LostNodeEvent' do
+        expect { execute_task }.to not_publish_event(Search::Zoekt::LostNodeEvent)
+      end
+    end
+
+    context 'when marking_lost_enabled? is true' do
+      before do
+        allow(Search::Zoekt::Node).to receive(:marking_lost_enabled?).and_return true
+      end
+
+      it 'publishes LostNodeEvent' do
+        expect { execute_task }.to publish_event(Search::Zoekt::LostNodeEvent).with({ zoekt_node_id: lost_node.id })
+      end
+    end
+  end
 end
diff --git a/ee/spec/workers/search/zoekt/lost_node_event_worker_spec.rb b/ee/spec/workers/search/zoekt/lost_node_event_worker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4fe4b1fbd507c7f1faaf7e8844710f0077ca11a8
--- /dev/null
+++ b/ee/spec/workers/search/zoekt/lost_node_event_worker_spec.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Search::Zoekt::LostNodeEventWorker, :zoekt_settings_enabled, feature_category: :global_search do
+  let_it_be_with_reload(:node) { create(:zoekt_node, :lost) }
+  let_it_be(:zoekt_index) { create(:zoekt_index, node: node) }
+  let_it_be(:zoekt_repository) { create(:zoekt_repository, zoekt_index: zoekt_index) }
+  let_it_be(:zoekt_task) { create(:zoekt_task, zoekt_repository: zoekt_repository, node: node) }
+  let(:data) do
+    { zoekt_node_id: node.id }
+  end
+
+  let(:event) { Search::Zoekt::LostNodeEvent.new(data: data) }
+  let(:worker) { described_class.new }
+
+  subject(:execute_event) { worker.perform(event.class.name, event.data) }
+
+  it_behaves_like 'worker with data consistency', described_class, data_consistency: :always
+
+  it_behaves_like 'subscribes to event'
+
+  it_behaves_like 'an idempotent worker' do
+    before do
+      allow(Search::Zoekt::Node).to receive(:marking_lost_enabled?).and_return true
+    end
+
+    context 'when node can not be found' do
+      let(:data) do
+        { zoekt_node_id: non_existing_record_id }
+      end
+
+      it 'does not deletes anything' do
+        expect { execute_event }.not_to change { Search::Zoekt::Node.count }
+        expect { execute_event }.not_to change { Search::Zoekt::Task.count }
+        expect { execute_event }.not_to change { Search::Zoekt::Repository.count }
+        expect { execute_event }.not_to change { Search::Zoekt::Index.count }
+      end
+    end
+
+    context 'when marking_lost_enabled? is false' do
+      before do
+        allow(Search::Zoekt::Node).to receive(:marking_lost_enabled?).and_return false
+      end
+
+      it 'does not deletes anything' do
+        expect { execute_event }.not_to change { Search::Zoekt::Node.count }
+        expect { execute_event }.not_to change { Search::Zoekt::Task.count }
+        expect { execute_event }.not_to change { Search::Zoekt::Repository.count }
+        expect { execute_event }.not_to change { Search::Zoekt::Index.count }
+      end
+    end
+
+    context 'when node is not lost' do
+      before do
+        node.update_column :last_seen_at, Time.zone.now
+      end
+
+      it 'does not deletes anything' do
+        expect { execute_event }.not_to change { Search::Zoekt::Node.count }
+        expect { execute_event }.not_to change { Search::Zoekt::Task.count }
+        expect { execute_event }.not_to change { Search::Zoekt::Repository.count }
+        expect { execute_event }.not_to change { Search::Zoekt::Index.count }
+      end
+    end
+
+    it 'deletes the given node, tasks, indices and repositories attached to this node' do
+      expect(Search::Zoekt::Node.all).to include(node)
+      expect(Search::Zoekt::Task.all).to include(zoekt_task)
+      expect(Search::Zoekt::Repository.all).to include(zoekt_repository)
+      expect(Search::Zoekt::Index.all).to include(zoekt_index)
+      tasks_count = node.tasks.count
+      indices_count = node.indices.count
+      repos_count = node.indices.reduce(0) { |sum, index| sum + index.zoekt_repositories.count }
+      log_data = {
+        node_id: node.id, node_name: node.metadata[:name], metadata: hash_including(
+          deleted_tasks_count: tasks_count, deleted_repos_count: repos_count, deleted_indices_count: indices_count,
+          transaction_time: a_kind_of(Float)
+        )
+      }
+      expect(worker).to receive(:log_hash_metadata_on_done).with(log_data)
+      execute_event
+      expect(Search::Zoekt::Node.all).not_to include(node)
+      expect(Search::Zoekt::Task.all).not_to include(zoekt_task)
+      expect(Search::Zoekt::Repository.all).not_to include(zoekt_repository)
+      expect(Search::Zoekt::Index.all).not_to include(zoekt_index)
+    end
+  end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 61a5a96d863b88de9039bc032fec0d778037d72e..c7087cb623dd7fcd29f24ccf7dbe356eabc4c447 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -18131,6 +18131,9 @@ msgstr ""
 msgid "Delete label: %{labelName}"
 msgstr ""
 
+msgid "Delete offline nodes automatically after %{label}"
+msgstr ""
+
 msgid "Delete pipeline"
 msgstr ""