diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index 3320c13e87bc5c0bf50b8eaf681c95e44dc88f0f..7e538238cbd133f348fac8bd922d703207015d73 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -50,8 +50,9 @@ def permanently_disabled?
   end
 
   # rubocop: disable CodeReuse/ServiceClass
-  def execute(data, hook_name)
-    WebHookService.new(self, data, hook_name).execute if executable?
+  def execute(data, hook_name, force: false)
+    # hook.executable? is checked in WebHookService#execute
+    WebHookService.new(self, data, hook_name, force: force).execute
   end
   # rubocop: enable CodeReuse/ServiceClass
 
diff --git a/app/services/test_hooks/base_service.rb b/app/services/test_hooks/base_service.rb
index 0fda6fb1ed074567c74d8aca808f7edb9d2d54da..b41a9959c138a202a1c947fe5e20353fde2a1112 100644
--- a/app/services/test_hooks/base_service.rb
+++ b/app/services/test_hooks/base_service.rb
@@ -18,7 +18,7 @@ def execute
       return error('Testing not available for this hook') if trigger_key.nil? || data.blank?
       return error(data[:error]) if data[:error].present?
 
-      hook.execute(data, trigger_key)
+      hook.execute(data, trigger_key, force: true)
     end
   end
 end
diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb
index a4342aa5b90436edd0800d4c2dc49079bf1d7692..b1d8872aa5e56dc50d6fe7f0182e305ead661753 100644
--- a/app/services/web_hook_service.rb
+++ b/app/services/web_hook_service.rb
@@ -34,11 +34,12 @@ def self.hook_to_event(hook_name)
     hook_name.to_s.singularize.titleize
   end
 
-  def initialize(hook, data, hook_name, uniqueness_token = nil)
+  def initialize(hook, data, hook_name, uniqueness_token = nil, force: false)
     @hook = hook
     @data = data
     @hook_name = hook_name.to_s
     @uniqueness_token = uniqueness_token
+    @force = force
     @request_options = {
       timeout: Gitlab.config.gitlab.webhook_timeout,
       use_read_total_timeout: true,
@@ -46,8 +47,12 @@ def initialize(hook, data, hook_name, uniqueness_token = nil)
     }
   end
 
+  def disabled?
+    !@force && !hook.executable?
+  end
+
   def execute
-    return { status: :error, message: 'Hook disabled' } unless hook.executable?
+    return { status: :error, message: 'Hook disabled' } if disabled?
 
     if recursion_blocked?
       log_recursion_blocked
@@ -148,8 +153,12 @@ def log_execution(trigger:, url:, request_data:, response:, execution_duration:,
       internal_error_message: error_message
     }
 
-    ::WebHooks::LogExecutionWorker
-      .perform_async(hook.id, log_data, category, uniqueness_token)
+    if @force # executed as part of test - run log-execution inline.
+      ::WebHooks::LogExecutionService.new(hook: hook, log_data: log_data, response_category: category).execute
+    else
+      ::WebHooks::LogExecutionWorker
+        .perform_async(hook.id, log_data, category, uniqueness_token)
+    end
   end
 
   def response_category(response)
diff --git a/spec/models/hooks/service_hook_spec.rb b/spec/models/hooks/service_hook_spec.rb
index 85f433f5f814b72169832ca78be0842e13b7fddb..0d65fe302e184c99aa21952c72d389171bd521fd 100644
--- a/spec/models/hooks/service_hook_spec.rb
+++ b/spec/models/hooks/service_hook_spec.rb
@@ -16,7 +16,7 @@
     let(:data) { { key: 'value' } }
 
     it '#execute' do
-      expect(WebHookService).to receive(:new).with(hook, data, 'service_hook').and_call_original
+      expect(WebHookService).to receive(:new).with(hook, data, 'service_hook', force: false).and_call_original
       expect_any_instance_of(WebHookService).to receive(:execute)
 
       hook.execute(data)
diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb
index 89bfb742f5d1a345cb7838e8faf1853504e58be6..a3d36058b743b8f5f2383dc5581b543cf47369a6 100644
--- a/spec/models/hooks/system_hook_spec.rb
+++ b/spec/models/hooks/system_hook_spec.rb
@@ -168,17 +168,17 @@
     let(:data) { { key: 'value' } }
     let(:hook_name) { 'system_hook' }
 
-    before do
-      expect(WebHookService).to receive(:new).with(hook, data, hook_name).and_call_original
-    end
-
     it '#execute' do
+      expect(WebHookService).to receive(:new).with(hook, data, hook_name, force: false).and_call_original
+
       expect_any_instance_of(WebHookService).to receive(:execute)
 
       hook.execute(data, hook_name)
     end
 
     it '#async_execute' do
+      expect(WebHookService).to receive(:new).with(hook, data, hook_name).and_call_original
+
       expect_any_instance_of(WebHookService).to receive(:async_execute)
 
       hook.async_execute(data, hook_name)
diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb
index c292e78b32d87c9592dce41503fdc4db0c766c4e..482e372543cd37cb2e11fb55f16de88c6f81b0dc 100644
--- a/spec/models/hooks/web_hook_spec.rb
+++ b/spec/models/hooks/web_hook_spec.rb
@@ -100,12 +100,18 @@
       hook.execute(data, hook_name)
     end
 
-    it 'does not execute non-executable hooks' do
-      hook.update!(disabled_until: 1.day.from_now)
+    it 'passes force: false to the web hook service by default' do
+      expect(WebHookService)
+        .to receive(:new).with(hook, data, hook_name, force: false).and_return(double(execute: :done))
 
-      expect(WebHookService).not_to receive(:new)
+      expect(hook.execute(data, hook_name)).to eq :done
+    end
 
-      hook.execute(data, hook_name)
+    it 'passes force: true to the web hook service if required' do
+      expect(WebHookService)
+        .to receive(:new).with(hook, data, hook_name, force: true).and_return(double(execute: :forced))
+
+      expect(hook.execute(data, hook_name, force: true)).to eq :forced
     end
 
     it '#async_execute' do
diff --git a/spec/services/test_hooks/project_service_spec.rb b/spec/services/test_hooks/project_service_spec.rb
index cd6284b4a87d38da84d48da4bbca93e32dc67e45..d97a6f15270b88e93935cdd9791780714c4d2234 100644
--- a/spec/services/test_hooks/project_service_spec.rb
+++ b/spec/services/test_hooks/project_service_spec.rb
@@ -37,7 +37,7 @@
       it 'executes hook' do
         allow(Gitlab::DataBuilder::Push).to receive(:build_sample).and_return(sample_data)
 
-        expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
+        expect(hook).to receive(:execute).with(sample_data, trigger_key, force: true).and_return(success_result)
         expect(service.execute).to include(success_result)
       end
     end
@@ -49,7 +49,7 @@
       it 'executes hook' do
         allow(Gitlab::DataBuilder::Push).to receive(:build_sample).and_return(sample_data)
 
-        expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
+        expect(hook).to receive(:execute).with(sample_data, trigger_key, force: true).and_return(success_result)
         expect(service.execute).to include(success_result)
       end
     end
@@ -69,7 +69,7 @@
         allow(Gitlab::DataBuilder::Note).to receive(:build).and_return(sample_data)
         allow_next(NotesFinder).to receive(:execute).and_return(Note.all)
 
-        expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
+        expect(hook).to receive(:execute).with(sample_data, trigger_key, force: true).and_return(success_result)
         expect(service.execute).to include(success_result)
       end
     end
@@ -86,7 +86,7 @@
         allow(issue).to receive(:to_hook_data).and_return(sample_data)
         allow_next(IssuesFinder).to receive(:execute).and_return([issue])
 
-        expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
+        expect(hook).to receive(:execute).with(sample_data, trigger_key, force: true).and_return(success_result)
         expect(service.execute).to include(success_result)
       end
     end
@@ -119,7 +119,7 @@
         allow(merge_request).to receive(:to_hook_data).and_return(sample_data)
         allow_next(MergeRequestsFinder).to receive(:execute).and_return([merge_request])
 
-        expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
+        expect(hook).to receive(:execute).with(sample_data, trigger_key, force: true).and_return(success_result)
         expect(service.execute).to include(success_result)
       end
     end
@@ -138,7 +138,7 @@
         allow(Gitlab::DataBuilder::Build).to receive(:build).and_return(sample_data)
         allow_next(Ci::JobsFinder).to receive(:execute).and_return([ci_job])
 
-        expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
+        expect(hook).to receive(:execute).with(sample_data, trigger_key, force: true).and_return(success_result)
         expect(service.execute).to include(success_result)
       end
     end
@@ -157,7 +157,7 @@
         allow(Gitlab::DataBuilder::Pipeline).to receive(:build).and_return(sample_data)
         allow_next(Ci::PipelinesFinder).to receive(:execute).and_return([pipeline])
 
-        expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
+        expect(hook).to receive(:execute).with(sample_data, trigger_key, force: true).and_return(success_result)
         expect(service.execute).to include(success_result)
       end
     end
@@ -184,7 +184,7 @@
         create(:wiki_page, wiki: project.wiki)
         allow(Gitlab::DataBuilder::WikiPage).to receive(:build).and_return(sample_data)
 
-        expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
+        expect(hook).to receive(:execute).with(sample_data, trigger_key, force: true).and_return(success_result)
         expect(service.execute).to include(success_result)
       end
     end
@@ -203,7 +203,7 @@
         allow(release).to receive(:to_hook_data).and_return(sample_data)
         allow_next(ReleasesFinder).to receive(:execute).and_return([release])
 
-        expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
+        expect(hook).to receive(:execute).with(sample_data, trigger_key, force: true).and_return(success_result)
         expect(service.execute).to include(success_result)
       end
     end
diff --git a/spec/services/test_hooks/system_service_spec.rb b/spec/services/test_hooks/system_service_spec.rb
index 48c8c24212ad1b1c175e5bd5513cb819a6018c5a..66a1218d123e1702281af1c6af0c592b5c04744a 100644
--- a/spec/services/test_hooks/system_service_spec.rb
+++ b/spec/services/test_hooks/system_service_spec.rb
@@ -32,7 +32,7 @@
       it 'executes hook' do
         expect(Gitlab::DataBuilder::Push).to receive(:sample_data).and_call_original
 
-        expect(hook).to receive(:execute).with(Gitlab::DataBuilder::Push::SAMPLE_DATA, trigger_key).and_return(success_result)
+        expect(hook).to receive(:execute).with(Gitlab::DataBuilder::Push::SAMPLE_DATA, trigger_key, force: true).and_return(success_result)
         expect(service.execute).to include(success_result)
       end
     end
@@ -45,7 +45,7 @@
         allow(project.repository).to receive(:tags).and_return(['tag'])
         expect(Gitlab::DataBuilder::Push).to receive(:sample_data).and_call_original
 
-        expect(hook).to receive(:execute).with(Gitlab::DataBuilder::Push::SAMPLE_DATA, trigger_key).and_return(success_result)
+        expect(hook).to receive(:execute).with(Gitlab::DataBuilder::Push::SAMPLE_DATA, trigger_key, force: true).and_return(success_result)
         expect(service.execute).to include(success_result)
       end
     end
@@ -57,7 +57,7 @@
       it 'executes hook' do
         expect(Gitlab::DataBuilder::Repository).to receive(:sample_data).and_call_original
 
-        expect(hook).to receive(:execute).with(Gitlab::DataBuilder::Repository::SAMPLE_DATA, trigger_key).and_return(success_result)
+        expect(hook).to receive(:execute).with(Gitlab::DataBuilder::Repository::SAMPLE_DATA, trigger_key, force: true).and_return(success_result)
         expect(service.execute).to include(success_result)
       end
     end
@@ -76,7 +76,7 @@
       it 'executes hook' do
         expect(MergeRequest).to receive(:of_projects).and_return([merge_request])
         expect(merge_request).to receive(:to_hook_data).and_return(sample_data)
-        expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
+        expect(hook).to receive(:execute).with(sample_data, trigger_key, force: true).and_return(success_result)
         expect(service.execute).to include(success_result)
       end
     end
diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb
index 448d00076c1efc0bbfa8533e01222d710e1923d5..64371f979086ecb495963936663f38fde5876b3b 100644
--- a/spec/services/web_hook_service_spec.rb
+++ b/spec/services/web_hook_service_spec.rb
@@ -52,6 +52,25 @@
     end
   end
 
+  describe '#disabled?' do
+    using RSpec::Parameterized::TableSyntax
+
+    subject { described_class.new(hook, data, :push_hooks, force: forced) }
+
+    let(:hook) { double(executable?: executable, allow_local_requests?: false) }
+
+    where(:forced, :executable, :disabled) do
+      false | true | false
+      false | false | true
+      true | true | false
+      true | false | false
+    end
+
+    with_them do
+      it { is_expected.to have_attributes(disabled?: disabled) }
+    end
+  end
+
   describe '#execute' do
     let!(:uuid) { SecureRandom.uuid }
     let(:headers) do
@@ -129,7 +148,7 @@
     end
 
     it 'does not execute disabled hooks' do
-      project_hook.update!(recent_failures: 4)
+      allow(service_instance).to receive(:disabled?).and_return(true)
 
       expect(service_instance.execute).to eq({ status: :error, message: 'Hook disabled' })
     end
@@ -251,6 +270,20 @@ def run_service
           stub_full_request(project_hook.url, method: :post).to_return(status: 200, body: 'Success')
         end
 
+        context 'when forced' do
+          let(:service_instance) { described_class.new(project_hook, data, :push_hooks, force: true) }
+
+          it 'logs execution inline' do
+            expect(::WebHooks::LogExecutionWorker).not_to receive(:perform_async)
+            expect(::WebHooks::LogExecutionService)
+              .to receive(:new)
+              .with(hook: project_hook, log_data: Hash, response_category: :ok)
+              .and_return(double(execute: nil))
+
+            run_service
+          end
+        end
+
         it 'log successful execution' do
           run_service