diff --git a/lib/api/ci/runners.rb b/lib/api/ci/runners.rb
index ec9b09a3419daacfeb238587c446fffc73eaa0f2..0048999a8d2fd9c2cfb8a47ecd022f386630e1c3 100644
--- a/lib/api/ci/runners.rb
+++ b/lib/api/ci/runners.rb
@@ -129,6 +129,14 @@ class Runners < ::API::Base
           authenticate_list_runners_jobs!(runner)
 
           jobs = ::Ci::RunnerJobsFinder.new(runner, current_user, params).execute
+          jobs = jobs.preload( # rubocop: disable CodeReuse/ActiveRecord
+            [
+              :user,
+              { pipeline: { project: [:route, { namespace: :route }] } },
+              { project: [:route, { namespace: :route }] }
+            ]
+          )
+          jobs.each(&:commit) # batch loads all commits
 
           present paginate(jobs), with: Entities::Ci::JobBasicWithProject
         end
diff --git a/spec/requests/api/ci/runners_spec.rb b/spec/requests/api/ci/runners_spec.rb
index 31b85a0b1d6d0b0519e46628b66bd9c2f28ae7de..a24bf22cd459d42b9c9c53223ec0356dae2296fa 100644
--- a/spec/requests/api/ci/runners_spec.rb
+++ b/spec/requests/api/ci/runners_spec.rb
@@ -889,6 +889,38 @@ def update_runner(id, user, args)
         end
       end
 
+      it 'avoids N+1 DB queries' do
+        get api("/runners/#{shared_runner.id}/jobs", admin)
+
+        control = ActiveRecord::QueryRecorder.new do
+          get api("/runners/#{shared_runner.id}/jobs", admin)
+        end
+
+        create(:ci_build, :failed, runner: shared_runner, project: create(:project))
+
+        expect do
+          get api("/runners/#{shared_runner.id}/jobs", admin)
+        end.not_to exceed_query_limit(control.count)
+      end
+
+      it 'batches loading of commits' do
+        shared_runner = create(:ci_runner, :instance, description: 'Shared runner')
+
+        project_with_repo = create(:project, :repository)
+
+        pipeline = create(:ci_pipeline, project: project_with_repo, sha: 'ddd0f15ae83993f5cb66a927a28673882e99100b')
+        create(:ci_build, :running, runner: shared_runner, project: project_with_repo, pipeline: pipeline)
+
+        pipeline = create(:ci_pipeline, project: project_with_repo, sha: 'c1c67abbaf91f624347bb3ae96eabe3a1b742478')
+        create(:ci_build, :failed, runner: shared_runner, project: project_with_repo, pipeline: pipeline)
+
+        expect_next_instance_of(Repository) do |repo|
+          expect(repo).to receive(:commits_by).once.and_call_original
+        end
+
+        get api("/runners/#{shared_runner.id}/jobs", admin)
+      end
+
       context "when runner doesn't exist" do
         it 'returns 404' do
           get api('/runners/0/jobs', admin)