diff --git a/app/assets/javascripts/serverless/components/functions.vue b/app/assets/javascripts/serverless/components/functions.vue
index f9b4e78956380593a17e32fde62a398f01730716..94341050b861bc4973d5af597bd7dfc9e075bde5 100644
--- a/app/assets/javascripts/serverless/components/functions.vue
+++ b/app/assets/javascripts/serverless/components/functions.vue
@@ -4,6 +4,7 @@ import { GlLoadingIcon } from '@gitlab/ui';
 import FunctionRow from './function_row.vue';
 import EnvironmentRow from './environment_row.vue';
 import EmptyState from './empty_state.vue';
+import { CHECKING_INSTALLED } from '../constants';
 
 export default {
   components: {
@@ -13,10 +14,6 @@ export default {
     GlLoadingIcon,
   },
   props: {
-    installed: {
-      type: Boolean,
-      required: true,
-    },
     clustersPath: {
       type: String,
       required: true,
@@ -31,8 +28,15 @@ export default {
     },
   },
   computed: {
-    ...mapState(['isLoading', 'hasFunctionData']),
+    ...mapState(['installed', 'isLoading', 'hasFunctionData']),
     ...mapGetters(['getFunctions']),
+
+    checkingInstalled() {
+      return this.installed === CHECKING_INSTALLED;
+    },
+    isInstalled() {
+      return this.installed === true;
+    },
   },
   created() {
     this.fetchFunctions({
@@ -47,15 +51,16 @@ export default {
 
 <template>
   <section id="serverless-functions">
-    <div v-if="installed">
+    <gl-loading-icon
+      v-if="checkingInstalled"
+      :size="2"
+      class="prepend-top-default append-bottom-default"
+    />
+
+    <div v-else-if="isInstalled">
       <div v-if="hasFunctionData">
-        <gl-loading-icon
-          v-if="isLoading"
-          :size="2"
-          class="prepend-top-default append-bottom-default"
-        />
-        <template v-else>
-          <div class="groups-list-tree-container">
+        <template>
+          <div class="groups-list-tree-container js-functions-wrapper">
             <ul class="content-list group-list-tree">
               <environment-row
                 v-for="(env, index) in getFunctions"
@@ -66,6 +71,11 @@ export default {
             </ul>
           </div>
         </template>
+        <gl-loading-icon
+          v-if="isLoading"
+          :size="2"
+          class="prepend-top-default append-bottom-default js-functions-loader"
+        />
       </div>
       <div v-else class="empty-state js-empty-state">
         <div class="text-content">
diff --git a/app/assets/javascripts/serverless/constants.js b/app/assets/javascripts/serverless/constants.js
index 35f77205f2ce9185dc4c152d0102f8587fc8a9fb..2fa15e56ccb67139744434d18556e8d01c1b86b1 100644
--- a/app/assets/javascripts/serverless/constants.js
+++ b/app/assets/javascripts/serverless/constants.js
@@ -1,3 +1,7 @@
 export const MAX_REQUESTS = 3; // max number of times to retry
 
 export const X_INTERVAL = 5; // Reflects the number of verticle bars on the x-axis
+
+export const CHECKING_INSTALLED = 'checking'; // The backend is still determining whether or not Knative is installed
+
+export const TIMEOUT = 'timeout';
diff --git a/app/assets/javascripts/serverless/serverless_bundle.js b/app/assets/javascripts/serverless/serverless_bundle.js
index 2d3f086ffeedf592e3cc4bb318df64f6560ded6b..ed3b633d7668015dbf1f5010f10443449fb95a1c 100644
--- a/app/assets/javascripts/serverless/serverless_bundle.js
+++ b/app/assets/javascripts/serverless/serverless_bundle.js
@@ -45,7 +45,7 @@ export default class Serverless {
         },
       });
     } else {
-      const { statusPath, clustersPath, helpPath, installed } = document.querySelector(
+      const { statusPath, clustersPath, helpPath } = document.querySelector(
         '.js-serverless-functions-page',
       ).dataset;
 
@@ -56,7 +56,6 @@ export default class Serverless {
         render(createElement) {
           return createElement(Functions, {
             props: {
-              installed: installed !== undefined,
               clustersPath,
               helpPath,
               statusPath,
diff --git a/app/assets/javascripts/serverless/store/actions.js b/app/assets/javascripts/serverless/store/actions.js
index 826501c90224ceba3e7c2bda676f40e52188d053..a0a9fdf7ace1be26196832da3e67748d5d9182ef 100644
--- a/app/assets/javascripts/serverless/store/actions.js
+++ b/app/assets/javascripts/serverless/store/actions.js
@@ -3,13 +3,18 @@ import axios from '~/lib/utils/axios_utils';
 import statusCodes from '~/lib/utils/http_status';
 import { backOff } from '~/lib/utils/common_utils';
 import createFlash from '~/flash';
-import { MAX_REQUESTS } from '../constants';
+import { __ } from '~/locale';
+import { MAX_REQUESTS, CHECKING_INSTALLED, TIMEOUT } from '../constants';
 
 export const requestFunctionsLoading = ({ commit }) => commit(types.REQUEST_FUNCTIONS_LOADING);
 export const receiveFunctionsSuccess = ({ commit }, data) =>
   commit(types.RECEIVE_FUNCTIONS_SUCCESS, data);
-export const receiveFunctionsNoDataSuccess = ({ commit }) =>
-  commit(types.RECEIVE_FUNCTIONS_NODATA_SUCCESS);
+export const receiveFunctionsPartial = ({ commit }, data) =>
+  commit(types.RECEIVE_FUNCTIONS_PARTIAL, data);
+export const receiveFunctionsTimeout = ({ commit }, data) =>
+  commit(types.RECEIVE_FUNCTIONS_TIMEOUT, data);
+export const receiveFunctionsNoDataSuccess = ({ commit }, data) =>
+  commit(types.RECEIVE_FUNCTIONS_NODATA_SUCCESS, data);
 export const receiveFunctionsError = ({ commit }, error) =>
   commit(types.RECEIVE_FUNCTIONS_ERROR, error);
 
@@ -25,18 +30,25 @@ export const receiveMetricsError = ({ commit }, error) =>
 export const fetchFunctions = ({ dispatch }, { functionsPath }) => {
   let retryCount = 0;
 
+  const functionsPartiallyFetched = data => {
+    if (data.functions !== null && data.functions.length) {
+      dispatch('receiveFunctionsPartial', data);
+    }
+  };
+
   dispatch('requestFunctionsLoading');
 
   backOff((next, stop) => {
     axios
       .get(functionsPath)
       .then(response => {
-        if (response.status === statusCodes.NO_CONTENT) {
+        if (response.data.knative_installed === CHECKING_INSTALLED) {
           retryCount += 1;
           if (retryCount < MAX_REQUESTS) {
+            functionsPartiallyFetched(response.data);
             next();
           } else {
-            stop(null);
+            stop(TIMEOUT);
           }
         } else {
           stop(response.data);
@@ -45,10 +57,13 @@ export const fetchFunctions = ({ dispatch }, { functionsPath }) => {
       .catch(stop);
   })
     .then(data => {
-      if (data !== null) {
+      if (data === TIMEOUT) {
+        dispatch('receiveFunctionsTimeout');
+        createFlash(__('Loading functions timed out. Please reload the page to try again.'));
+      } else if (data.functions !== null && data.functions.length) {
         dispatch('receiveFunctionsSuccess', data);
       } else {
-        dispatch('receiveFunctionsNoDataSuccess');
+        dispatch('receiveFunctionsNoDataSuccess', data);
       }
     })
     .catch(error => {
diff --git a/app/assets/javascripts/serverless/store/mutation_types.js b/app/assets/javascripts/serverless/store/mutation_types.js
index 25b2f7ac38aa20254a50725ea9c161b34d7eb0fe..b8fa9ea1a013fc0be3884d973a644a1f6c5090f1 100644
--- a/app/assets/javascripts/serverless/store/mutation_types.js
+++ b/app/assets/javascripts/serverless/store/mutation_types.js
@@ -1,5 +1,7 @@
 export const REQUEST_FUNCTIONS_LOADING = 'REQUEST_FUNCTIONS_LOADING';
 export const RECEIVE_FUNCTIONS_SUCCESS = 'RECEIVE_FUNCTIONS_SUCCESS';
+export const RECEIVE_FUNCTIONS_PARTIAL = 'RECEIVE_FUNCTIONS_PARTIAL';
+export const RECEIVE_FUNCTIONS_TIMEOUT = 'RECEIVE_FUNCTIONS_TIMEOUT';
 export const RECEIVE_FUNCTIONS_NODATA_SUCCESS = 'RECEIVE_FUNCTIONS_NODATA_SUCCESS';
 export const RECEIVE_FUNCTIONS_ERROR = 'RECEIVE_FUNCTIONS_ERROR';
 
diff --git a/app/assets/javascripts/serverless/store/mutations.js b/app/assets/javascripts/serverless/store/mutations.js
index 991f32a275d0352e03ae366a97ba83ca96b8cc6a..2685a5b11ffc8033f362346997cf14235ac40961 100644
--- a/app/assets/javascripts/serverless/store/mutations.js
+++ b/app/assets/javascripts/serverless/store/mutations.js
@@ -5,12 +5,23 @@ export default {
     state.isLoading = true;
   },
   [types.RECEIVE_FUNCTIONS_SUCCESS](state, data) {
-    state.functions = data;
+    state.functions = data.functions;
+    state.installed = data.knative_installed;
     state.isLoading = false;
     state.hasFunctionData = true;
   },
-  [types.RECEIVE_FUNCTIONS_NODATA_SUCCESS](state) {
+  [types.RECEIVE_FUNCTIONS_PARTIAL](state, data) {
+    state.functions = data.functions;
+    state.installed = true;
+    state.isLoading = true;
+    state.hasFunctionData = true;
+  },
+  [types.RECEIVE_FUNCTIONS_TIMEOUT](state) {
+    state.isLoading = false;
+  },
+  [types.RECEIVE_FUNCTIONS_NODATA_SUCCESS](state, data) {
     state.isLoading = false;
+    state.installed = data.knative_installed;
     state.hasFunctionData = false;
   },
   [types.RECEIVE_FUNCTIONS_ERROR](state, error) {
diff --git a/app/assets/javascripts/serverless/store/state.js b/app/assets/javascripts/serverless/store/state.js
index afc3f37d7ba3f3e81f55718bab5537309ba20c1e..fdd292997493f53f20c62adb72ab0bb1b6facb13 100644
--- a/app/assets/javascripts/serverless/store/state.js
+++ b/app/assets/javascripts/serverless/store/state.js
@@ -1,5 +1,6 @@
 export default () => ({
   error: null,
+  installed: 'checking',
   isLoading: true,
 
   // functions
diff --git a/app/controllers/projects/serverless/functions_controller.rb b/app/controllers/projects/serverless/functions_controller.rb
index 79030da64d34834a13534cae03b34e5a0825357a..4b0d001fca6a94fd52f3289cef50f1b1951b9e12 100644
--- a/app/controllers/projects/serverless/functions_controller.rb
+++ b/app/controllers/projects/serverless/functions_controller.rb
@@ -10,15 +10,13 @@ def index
           format.json do
             functions = finder.execute
 
-            if functions.any?
-              render json: serialize_function(functions)
-            else
-              head :no_content
-            end
+            render json: {
+              knative_installed: finder.knative_installed,
+              functions: serialize_function(functions)
+            }.to_json
           end
 
           format.html do
-            @installed = finder.installed?
             render
           end
         end
diff --git a/app/finders/clusters/knative_services_finder.rb b/app/finders/clusters/knative_services_finder.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7d3b53ef663149e6f3f930a4c69b3929fb071c06
--- /dev/null
+++ b/app/finders/clusters/knative_services_finder.rb
@@ -0,0 +1,112 @@
+# frozen_string_literal: true
+module Clusters
+  class KnativeServicesFinder
+    include ReactiveCaching
+    include Gitlab::Utils::StrongMemoize
+
+    KNATIVE_STATES = {
+      'checking' => 'checking',
+      'installed' => 'installed',
+      'not_found' => 'not_found'
+    }.freeze
+
+    self.reactive_cache_key = ->(finder) { finder.model_name }
+    self.reactive_cache_worker_finder = ->(_id, *cache_args) { from_cache(*cache_args) }
+
+    attr_reader :cluster, :project
+
+    def initialize(cluster, project)
+      @cluster = cluster
+      @project = project
+    end
+
+    def with_reactive_cache_memoized(*cache_args, &block)
+      strong_memoize(:reactive_cache) do
+        with_reactive_cache(*cache_args, &block)
+      end
+    end
+
+    def clear_cache!
+      clear_reactive_cache!(*cache_args)
+    end
+
+    def self.from_cache(cluster_id, project_id)
+      cluster = Clusters::Cluster.find(cluster_id)
+      project = ::Project.find(project_id)
+
+      new(cluster, project)
+    end
+
+    def calculate_reactive_cache(*)
+      # read_services calls knative_client.discover implicitily. If we stop
+      # detecting services but still want to detect knative, we'll need to
+      # explicitily call: knative_client.discover
+      #
+      # We didn't create it separately to avoid 2 cluster requests.
+      ksvc = read_services
+      pods = knative_client.discovered ? read_pods : []
+      { services: ksvc, pods: pods, knative_detected: knative_client.discovered }
+    end
+
+    def services
+      return [] unless search_namespace
+
+      cached_data = with_reactive_cache_memoized(*cache_args) { |data| data }
+      cached_data.to_h.fetch(:services, [])
+    end
+
+    def cache_args
+      [cluster.id, project.id]
+    end
+
+    def service_pod_details(service)
+      cached_data = with_reactive_cache_memoized(*cache_args) { |data| data }
+      cached_data.to_h.fetch(:pods, []).select do |pod|
+        filter_pods(pod, service)
+      end
+    end
+
+    def knative_detected
+      cached_data = with_reactive_cache_memoized(*cache_args) { |data| data }
+
+      knative_state = cached_data.to_h[:knative_detected]
+
+      return KNATIVE_STATES['checking'] if knative_state.nil?
+      return KNATIVE_STATES['installed'] if knative_state
+
+      KNATIVE_STATES['uninstalled']
+    end
+
+    def model_name
+      self.class.name.underscore.tr('/', '_')
+    end
+
+    private
+
+    def search_namespace
+      @search_namespace ||= cluster.kubernetes_namespace_for(project)
+    end
+
+    def knative_client
+      cluster.kubeclient.knative_client
+    end
+
+    def filter_pods(pod, service)
+      pod["metadata"]["labels"]["serving.knative.dev/service"] == service
+    end
+
+    def read_services
+      knative_client.get_services(namespace: search_namespace).as_json
+    rescue Kubeclient::ResourceNotFoundError
+      []
+    end
+
+    def read_pods
+      cluster.kubeclient.core_client.get_pods(namespace: search_namespace).as_json
+    end
+
+    def id
+      nil
+    end
+  end
+end
diff --git a/app/finders/projects/serverless/functions_finder.rb b/app/finders/projects/serverless/functions_finder.rb
index e5bffccabfe051df3084bd63ee1dc345bbddc1c2..ebe50806ca155fe00689d1c6b3c03fe62513fc1a 100644
--- a/app/finders/projects/serverless/functions_finder.rb
+++ b/app/finders/projects/serverless/functions_finder.rb
@@ -14,8 +14,16 @@ def execute
         knative_services.flatten.compact
       end
 
-      def installed?
-        clusters_with_knative_installed.exists?
+      # Possible return values: Clusters::KnativeServicesFinder::KNATIVE_STATE
+      def knative_installed
+        states = @clusters.map do |cluster|
+          cluster.application_knative
+          cluster.knative_services_finder(project).knative_detected.tap do |state|
+            return state if state == ::Clusters::KnativeServicesFinder::KNATIVE_STATES['checking'] # rubocop:disable Cop/AvoidReturnFromBlocks
+          end
+        end
+
+        states.any? { |state| state == ::Clusters::KnativeServicesFinder::KNATIVE_STATES['installed'] }
       end
 
       def service(environment_scope, name)
@@ -25,7 +33,7 @@ def service(environment_scope, name)
       def invocation_metrics(environment_scope, name)
         return unless prometheus_adapter&.can_query?
 
-        cluster = clusters_with_knative_installed.preload_knative.find do |c|
+        cluster = @clusters.find do |c|
           environment_scope == c.environment_scope
         end
 
@@ -34,7 +42,7 @@ def invocation_metrics(environment_scope, name)
       end
 
       def has_prometheus?(environment_scope)
-        clusters_with_knative_installed.preload_knative.to_a.any? do |cluster|
+        @clusters.any? do |cluster|
           environment_scope == cluster.environment_scope && cluster.application_prometheus_available?
         end
       end
@@ -42,10 +50,12 @@ def has_prometheus?(environment_scope)
       private
 
       def knative_service(environment_scope, name)
-        clusters_with_knative_installed.preload_knative.map do |cluster|
+        @clusters.map do |cluster|
           next if environment_scope != cluster.environment_scope
 
-          services = cluster.application_knative.services_for(ns: cluster.kubernetes_namespace_for(project))
+          services = cluster
+            .knative_services_finder(project)
+            .services
             .select { |svc| svc["metadata"]["name"] == name }
 
           add_metadata(cluster, services).first unless services.nil?
@@ -53,8 +63,11 @@ def knative_service(environment_scope, name)
       end
 
       def knative_services
-        clusters_with_knative_installed.preload_knative.map do |cluster|
-          services = cluster.application_knative.services_for(ns: cluster.kubernetes_namespace_for(project))
+        @clusters.map do |cluster|
+          services = cluster
+            .knative_services_finder(project)
+            .services
+
           add_metadata(cluster, services) unless services.nil?
         end
       end
@@ -65,17 +78,14 @@ def add_metadata(cluster, services)
           s["cluster_id"] = cluster.id
 
           if services.length == 1
-            s["podcount"] = cluster.application_knative.service_pod_details(
-              cluster.kubernetes_namespace_for(project),
-              s["metadata"]["name"]).length
+            s["podcount"] = cluster
+              .knative_services_finder(project)
+              .service_pod_details(s["metadata"]["name"])
+              .length
           end
         end
       end
 
-      def clusters_with_knative_installed
-        @clusters.with_knative_installed
-      end
-
       # rubocop: disable CodeReuse/ServiceClass
       def prometheus_adapter
         @prometheus_adapter ||= ::Prometheus::AdapterService.new(project).prometheus_adapter
diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb
index 9fbf5d8af045d1c677b9adfbcea00dc7258a59c4..d5a3bd62e3d762be87210a52c30c9c24c9dd01c9 100644
--- a/app/models/clusters/applications/knative.rb
+++ b/app/models/clusters/applications/knative.rb
@@ -15,9 +15,6 @@ class Knative < ApplicationRecord
       include ::Clusters::Concerns::ApplicationVersion
       include ::Clusters::Concerns::ApplicationData
       include AfterCommitQueue
-      include ReactiveCaching
-
-      self.reactive_cache_key = ->(knative) { [knative.class.model_name.singular, knative.id] }
 
       def set_initial_status
         return unless not_installable?
@@ -41,8 +38,6 @@ def set_initial_status
 
       scope :for_cluster, -> (cluster) { where(cluster: cluster) }
 
-      after_save :clear_reactive_cache!
-
       def chart
         'knative/knative'
       end
@@ -77,55 +72,12 @@ def schedule_status_update
         ClusterWaitForIngressIpAddressWorker.perform_async(name, id)
       end
 
-      def client
-        cluster.kubeclient.knative_client
-      end
-
-      def services
-        with_reactive_cache do |data|
-          data[:services]
-        end
-      end
-
-      def calculate_reactive_cache
-        { services: read_services, pods: read_pods }
-      end
-
       def ingress_service
         cluster.kubeclient.get_service('istio-ingressgateway', 'istio-system')
       end
 
-      def services_for(ns: namespace)
-        return [] unless services
-        return [] unless ns
-
-        services.select do |service|
-          service.dig('metadata', 'namespace') == ns
-        end
-      end
-
-      def service_pod_details(ns, service)
-        with_reactive_cache do |data|
-          data[:pods].select { |pod| filter_pods(pod, ns, service) }
-        end
-      end
-
       private
 
-      def read_pods
-        cluster.kubeclient.core_client.get_pods.as_json
-      end
-
-      def filter_pods(pod, namespace, service)
-        pod["metadata"]["namespace"] == namespace && pod["metadata"]["labels"]["serving.knative.dev/service"] == service
-      end
-
-      def read_services
-        client.get_services.as_json
-      rescue Kubeclient::ResourceNotFoundError
-        []
-      end
-
       def install_knative_metrics
         ["kubectl apply -f #{METRICS_CONFIG}"] if cluster.application_prometheus_available?
       end
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index e9bbecdbecbcda158a1b6e91c7ca33c763e103cf..66cf690ab50fe79fd4fd81f03b9e04b888c7c5de 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -223,6 +223,10 @@ def predefined_variables
       end
     end
 
+    def knative_services_finder(project)
+      @knative_services_finder ||= KnativeServicesFinder.new(self, project)
+    end
+
     private
 
     def instance_domain
diff --git a/changelogs/unreleased/58941-use-gitlab-serverless-with-existing-knative-installation.yml b/changelogs/unreleased/58941-use-gitlab-serverless-with-existing-knative-installation.yml
new file mode 100644
index 0000000000000000000000000000000000000000..53be008816d5d6a801208ebb9f513ae8ba25619e
--- /dev/null
+++ b/changelogs/unreleased/58941-use-gitlab-serverless-with-existing-knative-installation.yml
@@ -0,0 +1,5 @@
+---
+title: Enable function features for external Knative installations
+merge_request: 27173
+author:
+type: changed
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 3c586501f3847d73b08fb00a03e4282f63174e7e..f9698d5ff06d3e7ef97938ff4e9c68f72b1b6755 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -7775,6 +7775,9 @@ msgstr ""
 msgid "Loading contribution stats for group members"
 msgstr ""
 
+msgid "Loading functions timed out. Please reload the page to try again."
+msgstr ""
+
 msgid "Loading the GitLab IDE..."
 msgstr ""
 
diff --git a/spec/controllers/projects/serverless/functions_controller_spec.rb b/spec/controllers/projects/serverless/functions_controller_spec.rb
index 782f5f272d92182efcfd072aa8aa6bf8699d1251..18c594acae030cb85cb249b076cd1aedd097908e 100644
--- a/spec/controllers/projects/serverless/functions_controller_spec.rb
+++ b/spec/controllers/projects/serverless/functions_controller_spec.rb
@@ -8,9 +8,8 @@
 
   let(:user) { create(:user) }
   let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
-  let(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) }
   let(:service) { cluster.platform_kubernetes }
-  let(:project) { cluster.project}
+  let(:project) { cluster.project }
 
   let(:namespace) do
     create(:cluster_kubernetes_namespace,
@@ -30,17 +29,69 @@ def params(opts = {})
   end
 
   describe 'GET #index' do
-    context 'empty cache' do
-      it 'has no data' do
+    let(:expected_json) { { 'knative_installed' => knative_state, 'functions' => functions } }
+
+    context 'when cache is being read' do
+      let(:knative_state) { 'checking' }
+      let(:functions) { [] }
+
+      before do
         get :index, params: params({ format: :json })
+      end
 
-        expect(response).to have_gitlab_http_status(204)
+      it 'returns checking' do
+        expect(json_response).to eq expected_json
       end
 
-      it 'renders an html page' do
-        get :index, params: params
+      it { expect(response).to have_gitlab_http_status(200) }
+    end
+
+    context 'when cache is ready' do
+      let(:knative_services_finder) { project.clusters.first.knative_services_finder(project) }
+      let(:knative_state) { true }
 
-        expect(response).to have_gitlab_http_status(200)
+      before do
+        allow_any_instance_of(Clusters::Cluster)
+          .to receive(:knative_services_finder)
+          .and_return(knative_services_finder)
+        synchronous_reactive_cache(knative_services_finder)
+        stub_kubeclient_service_pods(
+          kube_response({ "kind" => "PodList", "items" => [] }),
+          namespace: namespace.namespace
+        )
+      end
+
+      context 'when no functions were found' do
+        let(:functions) { [] }
+
+        before do
+          stub_kubeclient_knative_services(
+            namespace: namespace.namespace,
+            response: kube_response({ "kind" => "ServiceList", "items" => [] })
+          )
+          get :index, params: params({ format: :json })
+        end
+
+        it 'returns checking' do
+          expect(json_response).to eq expected_json
+        end
+
+        it { expect(response).to have_gitlab_http_status(200) }
+      end
+
+      context 'when functions were found' do
+        let(:functions) { ["asdf"] }
+
+        before do
+          stub_kubeclient_knative_services(namespace: namespace.namespace)
+          get :index, params: params({ format: :json })
+        end
+
+        it 'returns functions' do
+          expect(json_response["functions"]).not_to be_empty
+        end
+
+        it { expect(response).to have_gitlab_http_status(200) }
       end
     end
   end
@@ -56,11 +107,12 @@ def params(opts = {})
     context 'valid data', :use_clean_rails_memory_store_caching do
       before do
         stub_kubeclient_service_pods
-        stub_reactive_cache(knative,
+        stub_reactive_cache(cluster.knative_services_finder(project),
           {
             services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
             pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
-          })
+          },
+          *cluster.knative_services_finder(project).cache_args)
       end
 
       it 'has a valid function name' do
@@ -88,11 +140,12 @@ def params(opts = {})
   describe 'GET #index with data', :use_clean_rails_memory_store_caching do
     before do
       stub_kubeclient_service_pods
-      stub_reactive_cache(knative,
+      stub_reactive_cache(cluster.knative_services_finder(project),
         {
           services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
           pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
-        })
+        },
+        *cluster.knative_services_finder(project).cache_args)
     end
 
     it 'has data' do
@@ -100,11 +153,16 @@ def params(opts = {})
 
       expect(response).to have_gitlab_http_status(200)
 
-      expect(json_response).to contain_exactly(
-        a_hash_including(
-          "name" => project.name,
-          "url" => "http://#{project.name}.#{namespace.namespace}.example.com"
-        )
+      expect(json_response).to match(
+        {
+          "knative_installed" => "checking",
+          "functions" => [
+            a_hash_including(
+              "name" => project.name,
+              "url" => "http://#{project.name}.#{namespace.namespace}.example.com"
+            )
+          ]
+        }
       )
     end
 
diff --git a/spec/features/projects/serverless/functions_spec.rb b/spec/features/projects/serverless/functions_spec.rb
index e14934b16724da6afa53569717fce29160630894..9865dbbfb3c3e66cf1539211a5b66aa36f1aad56 100644
--- a/spec/features/projects/serverless/functions_spec.rb
+++ b/spec/features/projects/serverless/functions_spec.rb
@@ -4,6 +4,7 @@
 
 describe 'Functions', :js do
   include KubernetesHelpers
+  include ReactiveCachingHelpers
 
   let(:project) { create(:project) }
   let(:user) { create(:user) }
@@ -13,44 +14,70 @@
     gitlab_sign_in(user)
   end
 
-  context 'when user does not have a cluster and visits the serverless page' do
+  shared_examples "it's missing knative installation" do
     before do
       visit project_serverless_functions_path(project)
     end
 
-    it 'sees an empty state' do
+    it 'sees an empty state require Knative installation' do
       expect(page).to have_link('Install Knative')
       expect(page).to have_selector('.empty-state')
     end
   end
 
+  context 'when user does not have a cluster and visits the serverless page' do
+    it_behaves_like "it's missing knative installation"
+  end
+
   context 'when the user does have a cluster and visits the serverless page' do
     let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
 
-    before do
-      visit project_serverless_functions_path(project)
-    end
-
-    it 'sees an empty state' do
-      expect(page).to have_link('Install Knative')
-      expect(page).to have_selector('.empty-state')
-    end
+    it_behaves_like "it's missing knative installation"
   end
 
   context 'when the user has a cluster and knative installed and visits the serverless page' do
     let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
     let(:service) { cluster.platform_kubernetes }
-    let(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) }
-    let(:project) { knative.cluster.project }
+    let(:project) { cluster.project }
+    let(:knative_services_finder) { project.clusters.first.knative_services_finder(project) }
+    let(:namespace) do
+      create(:cluster_kubernetes_namespace,
+        cluster: cluster,
+        cluster_project: cluster.cluster_project,
+        project: cluster.cluster_project.project)
+    end
 
     before do
-      stub_kubeclient_knative_services
-      stub_kubeclient_service_pods
+      allow_any_instance_of(Clusters::Cluster)
+        .to receive(:knative_services_finder)
+        .and_return(knative_services_finder)
+      synchronous_reactive_cache(knative_services_finder)
+      stub_kubeclient_knative_services(stub_get_services_options)
+      stub_kubeclient_service_pods(nil, namespace: namespace.namespace)
       visit project_serverless_functions_path(project)
     end
 
-    it 'sees an empty listing of serverless functions' do
-      expect(page).to have_selector('.empty-state')
+    context 'when there are no functions' do
+      let(:stub_get_services_options) do
+        {
+          namespace: namespace.namespace,
+          response: kube_response({ "kind" => "ServiceList", "items" => [] })
+        }
+      end
+
+      it 'sees an empty listing of serverless functions' do
+        expect(page).to have_selector('.empty-state')
+        expect(page).not_to have_selector('.content-list')
+      end
+    end
+
+    context 'when there are functions' do
+      let(:stub_get_services_options) { { namespace: namespace.namespace } }
+
+      it 'does not see an empty listing of serverless functions' do
+        expect(page).not_to have_selector('.empty-state')
+        expect(page).to have_selector('.content-list')
+      end
     end
   end
 end
diff --git a/spec/finders/clusters/knative_services_finder_spec.rb b/spec/finders/clusters/knative_services_finder_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b731c2bd6bf990be618bd868883b2efd0aae7ea3
--- /dev/null
+++ b/spec/finders/clusters/knative_services_finder_spec.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::KnativeServicesFinder do
+  include KubernetesHelpers
+  include ReactiveCachingHelpers
+
+  let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+  let(:service) { cluster.platform_kubernetes }
+  let(:project) { cluster.cluster_project.project }
+  let(:namespace) do
+    create(:cluster_kubernetes_namespace,
+      cluster: cluster,
+      cluster_project: cluster.cluster_project,
+      project: project)
+  end
+
+  before do
+    stub_kubeclient_knative_services(namespace: namespace.namespace)
+    stub_kubeclient_service_pods(
+      kube_response(
+        kube_knative_pods_body(
+          project.name, namespace.namespace
+        )
+      ),
+      namespace: namespace.namespace
+    )
+  end
+
+  shared_examples 'a cached data' do
+    it 'has an unintialized cache' do
+      is_expected.to be_blank
+    end
+
+    context 'when using synchronous reactive cache' do
+      before do
+        synchronous_reactive_cache(cluster.knative_services_finder(project))
+      end
+
+      context 'when there are functions for cluster namespace' do
+        it { is_expected.not_to be_blank }
+      end
+
+      context 'when there are no functions for cluster namespace' do
+        before do
+          stub_kubeclient_knative_services(
+            namespace: namespace.namespace,
+            response: kube_response({ "kind" => "ServiceList", "items" => [] })
+          )
+          stub_kubeclient_service_pods(
+            kube_response({ "kind" => "PodList", "items" => [] }),
+            namespace: namespace.namespace
+          )
+        end
+
+        it { is_expected.to be_blank }
+      end
+    end
+  end
+
+  describe '#service_pod_details' do
+    subject { cluster.knative_services_finder(project).service_pod_details(project.name) }
+
+    it_behaves_like 'a cached data'
+  end
+
+  describe '#services' do
+    subject { cluster.knative_services_finder(project).services }
+
+    it_behaves_like 'a cached data'
+  end
+
+  describe '#knative_detected' do
+    subject { cluster.knative_services_finder(project).knative_detected }
+    before do
+      synchronous_reactive_cache(cluster.knative_services_finder(project))
+    end
+
+    context 'when knative is installed' do
+      before do
+        stub_kubeclient_discover(service.api_url)
+      end
+
+      it { is_expected.to be_truthy }
+      it "discovers knative installation" do
+        expect { subject }
+          .to change { cluster.kubeclient.knative_client.discovered }
+          .from(false)
+          .to(true)
+      end
+    end
+
+    context 'when knative is not installed' do
+      before do
+        stub_kubeclient_discover_knative_not_found(service.api_url)
+      end
+
+      it { is_expected.to be_falsy }
+      it "does not discover knative installation" do
+        expect { subject }.not_to change { cluster.kubeclient.knative_client.discovered }
+      end
+    end
+  end
+end
diff --git a/spec/finders/projects/serverless/functions_finder_spec.rb b/spec/finders/projects/serverless/functions_finder_spec.rb
index 3ad38207da4a2fd6f8b6163705addb94fecb0411..8aea45b457c24e65fd886e6214e8c57a1c4dea16 100644
--- a/spec/finders/projects/serverless/functions_finder_spec.rb
+++ b/spec/finders/projects/serverless/functions_finder_spec.rb
@@ -10,7 +10,7 @@
   let(:user) { create(:user) }
   let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
   let(:service) { cluster.platform_kubernetes }
-  let(:project) { cluster.project}
+  let(:project) { cluster.project }
 
   let(:namespace) do
     create(:cluster_kubernetes_namespace,
@@ -23,9 +23,45 @@
     project.add_maintainer(user)
   end
 
+  describe '#installed' do
+    it 'when reactive_caching is still fetching data' do
+      expect(described_class.new(project).knative_installed).to eq 'checking'
+    end
+
+    context 'when reactive_caching has finished' do
+      let(:knative_services_finder) { project.clusters.first.knative_services_finder(project) }
+
+      before do
+        allow_any_instance_of(Clusters::Cluster)
+          .to receive(:knative_services_finder)
+          .and_return(knative_services_finder)
+        synchronous_reactive_cache(knative_services_finder)
+      end
+
+      context 'when knative is not installed' do
+        it 'returns false' do
+          stub_kubeclient_discover_knative_not_found(service.api_url)
+
+          expect(described_class.new(project).knative_installed).to eq false
+        end
+      end
+
+      context 'reactive_caching is finished and knative is installed' do
+        let(:knative_services_finder) { project.clusters.first.knative_services_finder(project) }
+
+        it 'returns true' do
+          stub_kubeclient_knative_services(namespace: namespace.namespace)
+          stub_kubeclient_service_pods(nil, namespace: namespace.namespace)
+
+          expect(described_class.new(project).knative_installed).to be true
+        end
+      end
+    end
+  end
+
   describe 'retrieve data from knative' do
-    it 'does not have knative installed' do
-      expect(described_class.new(project).execute).to be_empty
+    context 'does not have knative installed' do
+      it { expect(described_class.new(project).execute).to be_empty }
     end
 
     context 'has knative installed' do
@@ -38,22 +74,24 @@
 
       it 'there are functions', :use_clean_rails_memory_store_caching do
         stub_kubeclient_service_pods
-        stub_reactive_cache(knative,
+        stub_reactive_cache(cluster.knative_services_finder(project),
           {
             services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
             pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
-          })
+          },
+          *cluster.knative_services_finder(project).cache_args)
 
         expect(finder.execute).not_to be_empty
       end
 
       it 'has a function', :use_clean_rails_memory_store_caching do
         stub_kubeclient_service_pods
-        stub_reactive_cache(knative,
+        stub_reactive_cache(cluster.knative_services_finder(project),
           {
             services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
             pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
-          })
+          },
+          *cluster.knative_services_finder(project).cache_args)
 
         result = finder.service(cluster.environment_scope, cluster.project.name)
         expect(result).not_to be_empty
@@ -84,20 +122,4 @@
       end
     end
   end
-
-  describe 'verify if knative is installed' do
-    context 'knative is not installed' do
-      it 'does not have knative installed' do
-        expect(described_class.new(project).installed?).to be false
-      end
-    end
-
-    context 'knative is installed' do
-      let!(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) }
-
-      it 'does have knative installed' do
-        expect(described_class.new(project).installed?).to be true
-      end
-    end
-  end
 end
diff --git a/spec/frontend/serverless/components/environment_row_spec.js b/spec/frontend/serverless/components/environment_row_spec.js
index 161a637dd75a3e39314b4c5513bde9be8c8f8fdb..0ad85e218dc3e9a0cfa0ba00865cfd3b0e7bfeb5 100644
--- a/spec/frontend/serverless/components/environment_row_spec.js
+++ b/spec/frontend/serverless/components/environment_row_spec.js
@@ -14,7 +14,7 @@ describe('environment row component', () => {
 
     beforeEach(() => {
       localVue = createLocalVue();
-      vm = createComponent(localVue, translate(mockServerlessFunctions)['*'], '*');
+      vm = createComponent(localVue, translate(mockServerlessFunctions.functions)['*'], '*');
     });
 
     afterEach(() => vm.$destroy());
@@ -48,7 +48,11 @@ describe('environment row component', () => {
 
     beforeEach(() => {
       localVue = createLocalVue();
-      vm = createComponent(localVue, translate(mockServerlessFunctionsDiffEnv).test, 'test');
+      vm = createComponent(
+        localVue,
+        translate(mockServerlessFunctionsDiffEnv.functions).test,
+        'test',
+      );
     });
 
     afterEach(() => vm.$destroy());
diff --git a/spec/frontend/serverless/components/functions_spec.js b/spec/frontend/serverless/components/functions_spec.js
index 6924fb9e91f8bd3cb5f32ff6788bb52798d288fe..d8a80f8031e2cb0d855cd4185dee85a2084125b8 100644
--- a/spec/frontend/serverless/components/functions_spec.js
+++ b/spec/frontend/serverless/components/functions_spec.js
@@ -34,11 +34,11 @@ describe('functionsComponent', () => {
   });
 
   it('should render empty state when Knative is not installed', () => {
+    store.dispatch('receiveFunctionsSuccess', { knative_installed: false });
     component = shallowMount(functionsComponent, {
       localVue,
       store,
       propsData: {
-        installed: false,
         clustersPath: '',
         helpPath: '',
         statusPath: '',
@@ -55,7 +55,6 @@ describe('functionsComponent', () => {
       localVue,
       store,
       propsData: {
-        installed: true,
         clustersPath: '',
         helpPath: '',
         statusPath: '',
@@ -67,12 +66,11 @@ describe('functionsComponent', () => {
   });
 
   it('should render empty state when there is no function data', () => {
-    store.dispatch('receiveFunctionsNoDataSuccess');
+    store.dispatch('receiveFunctionsNoDataSuccess', { knative_installed: true });
     component = shallowMount(functionsComponent, {
       localVue,
       store,
       propsData: {
-        installed: true,
         clustersPath: '',
         helpPath: '',
         statusPath: '',
@@ -91,12 +89,31 @@ describe('functionsComponent', () => {
     );
   });
 
+  it('should render functions and a loader when functions are partially fetched', () => {
+    store.dispatch('receiveFunctionsPartial', {
+      ...mockServerlessFunctions,
+      knative_installed: 'checking',
+    });
+    component = shallowMount(functionsComponent, {
+      localVue,
+      store,
+      propsData: {
+        clustersPath: '',
+        helpPath: '',
+        statusPath: '',
+      },
+      sync: false,
+    });
+
+    expect(component.find('.js-functions-wrapper').exists()).toBe(true);
+    expect(component.find('.js-functions-loader').exists()).toBe(true);
+  });
+
   it('should render the functions list', () => {
     component = shallowMount(functionsComponent, {
       localVue,
       store,
       propsData: {
-        installed: true,
         clustersPath: 'clustersPath',
         helpPath: 'helpPath',
         statusPath,
diff --git a/spec/frontend/serverless/mock_data.js b/spec/frontend/serverless/mock_data.js
index a2c1861632435682786f147a4623e96c49092bc8..ef616ceb37f41db47a80b8914324ee39983d79fb 100644
--- a/spec/frontend/serverless/mock_data.js
+++ b/spec/frontend/serverless/mock_data.js
@@ -1,56 +1,62 @@
-export const mockServerlessFunctions = [
-  {
-    name: 'testfunc1',
-    namespace: 'tm-example',
-    environment_scope: '*',
-    cluster_id: 46,
-    detail_url: '/testuser/testproj/serverless/functions/*/testfunc1',
-    podcount: null,
-    created_at: '2019-02-05T01:01:23Z',
-    url: 'http://testfunc1.tm-example.apps.example.com',
-    description: 'A test service',
-    image: 'knative-test-container-buildtemplate',
-  },
-  {
-    name: 'testfunc2',
-    namespace: 'tm-example',
-    environment_scope: '*',
-    cluster_id: 46,
-    detail_url: '/testuser/testproj/serverless/functions/*/testfunc2',
-    podcount: null,
-    created_at: '2019-02-05T01:01:23Z',
-    url: 'http://testfunc2.tm-example.apps.example.com',
-    description: 'A second test service\nThis one with additional descriptions',
-    image: 'knative-test-echo-buildtemplate',
-  },
-];
+export const mockServerlessFunctions = {
+  knative_installed: true,
+  functions: [
+    {
+      name: 'testfunc1',
+      namespace: 'tm-example',
+      environment_scope: '*',
+      cluster_id: 46,
+      detail_url: '/testuser/testproj/serverless/functions/*/testfunc1',
+      podcount: null,
+      created_at: '2019-02-05T01:01:23Z',
+      url: 'http://testfunc1.tm-example.apps.example.com',
+      description: 'A test service',
+      image: 'knative-test-container-buildtemplate',
+    },
+    {
+      name: 'testfunc2',
+      namespace: 'tm-example',
+      environment_scope: '*',
+      cluster_id: 46,
+      detail_url: '/testuser/testproj/serverless/functions/*/testfunc2',
+      podcount: null,
+      created_at: '2019-02-05T01:01:23Z',
+      url: 'http://testfunc2.tm-example.apps.example.com',
+      description: 'A second test service\nThis one with additional descriptions',
+      image: 'knative-test-echo-buildtemplate',
+    },
+  ],
+};
 
-export const mockServerlessFunctionsDiffEnv = [
-  {
-    name: 'testfunc1',
-    namespace: 'tm-example',
-    environment_scope: '*',
-    cluster_id: 46,
-    detail_url: '/testuser/testproj/serverless/functions/*/testfunc1',
-    podcount: null,
-    created_at: '2019-02-05T01:01:23Z',
-    url: 'http://testfunc1.tm-example.apps.example.com',
-    description: 'A test service',
-    image: 'knative-test-container-buildtemplate',
-  },
-  {
-    name: 'testfunc2',
-    namespace: 'tm-example',
-    environment_scope: 'test',
-    cluster_id: 46,
-    detail_url: '/testuser/testproj/serverless/functions/*/testfunc2',
-    podcount: null,
-    created_at: '2019-02-05T01:01:23Z',
-    url: 'http://testfunc2.tm-example.apps.example.com',
-    description: 'A second test service\nThis one with additional descriptions',
-    image: 'knative-test-echo-buildtemplate',
-  },
-];
+export const mockServerlessFunctionsDiffEnv = {
+  knative_installed: true,
+  functions: [
+    {
+      name: 'testfunc1',
+      namespace: 'tm-example',
+      environment_scope: '*',
+      cluster_id: 46,
+      detail_url: '/testuser/testproj/serverless/functions/*/testfunc1',
+      podcount: null,
+      created_at: '2019-02-05T01:01:23Z',
+      url: 'http://testfunc1.tm-example.apps.example.com',
+      description: 'A test service',
+      image: 'knative-test-container-buildtemplate',
+    },
+    {
+      name: 'testfunc2',
+      namespace: 'tm-example',
+      environment_scope: 'test',
+      cluster_id: 46,
+      detail_url: '/testuser/testproj/serverless/functions/*/testfunc2',
+      podcount: null,
+      created_at: '2019-02-05T01:01:23Z',
+      url: 'http://testfunc2.tm-example.apps.example.com',
+      description: 'A second test service\nThis one with additional descriptions',
+      image: 'knative-test-echo-buildtemplate',
+    },
+  ],
+};
 
 export const mockServerlessFunction = {
   name: 'testfunc1',
diff --git a/spec/frontend/serverless/store/getters_spec.js b/spec/frontend/serverless/store/getters_spec.js
index fb549c8f153fcfa1611e1052b2e2a687b56b0485..92853fda37c4a3de822eed0937b2732aa18c3c0f 100644
--- a/spec/frontend/serverless/store/getters_spec.js
+++ b/spec/frontend/serverless/store/getters_spec.js
@@ -32,7 +32,7 @@ describe('Serverless Store Getters', () => {
 
   describe('getFunctions', () => {
     it('should translate the raw function array to group the functions per environment scope', () => {
-      state.functions = mockServerlessFunctions;
+      state.functions = mockServerlessFunctions.functions;
 
       const funcs = getters.getFunctions(state);
 
diff --git a/spec/frontend/serverless/store/mutations_spec.js b/spec/frontend/serverless/store/mutations_spec.js
index ca3053e5c384fe6d2e9df56e6bc42bf55d739800..e2771c7e5fd47e59232ac687166539c4e565345e 100644
--- a/spec/frontend/serverless/store/mutations_spec.js
+++ b/spec/frontend/serverless/store/mutations_spec.js
@@ -19,13 +19,13 @@ describe('ServerlessMutations', () => {
 
       expect(state.isLoading).toEqual(false);
       expect(state.hasFunctionData).toEqual(true);
-      expect(state.functions).toEqual(mockServerlessFunctions);
+      expect(state.functions).toEqual(mockServerlessFunctions.functions);
     });
 
     it('should ensure loading has stopped and hasFunctionData is false when there are no functions available', () => {
       const state = {};
 
-      mutations[types.RECEIVE_FUNCTIONS_NODATA_SUCCESS](state);
+      mutations[types.RECEIVE_FUNCTIONS_NODATA_SUCCESS](state, { knative_installed: true });
 
       expect(state.isLoading).toEqual(false);
       expect(state.hasFunctionData).toEqual(false);
diff --git a/spec/models/clusters/applications/knative_spec.rb b/spec/models/clusters/applications/knative_spec.rb
index d5974f47190764c3e6850f6dfa927c03686219d3..b38cf96de7ebe4663dfc8265871592425a7a6afa 100644
--- a/spec/models/clusters/applications/knative_spec.rb
+++ b/spec/models/clusters/applications/knative_spec.rb
@@ -3,9 +3,6 @@
 require 'rails_helper'
 
 describe Clusters::Applications::Knative do
-  include KubernetesHelpers
-  include ReactiveCachingHelpers
-
   let(:knative) { create(:clusters_applications_knative) }
 
   include_examples 'cluster application core specs', :clusters_applications_knative
@@ -146,77 +143,4 @@
   describe 'validations' do
     it { is_expected.to validate_presence_of(:hostname) }
   end
-
-  describe '#service_pod_details' do
-    let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
-    let(:service) { cluster.platform_kubernetes }
-    let(:knative) { create(:clusters_applications_knative, cluster: cluster) }
-
-    let(:namespace) do
-      create(:cluster_kubernetes_namespace,
-        cluster: cluster,
-        cluster_project: cluster.cluster_project,
-        project: cluster.cluster_project.project)
-    end
-
-    before do
-      stub_kubeclient_discover(service.api_url)
-      stub_kubeclient_knative_services
-      stub_kubeclient_service_pods
-      stub_reactive_cache(knative,
-        {
-          services: kube_response(kube_knative_services_body),
-          pods: kube_response(kube_knative_pods_body(cluster.cluster_project.project.name, namespace.namespace))
-        })
-      synchronous_reactive_cache(knative)
-    end
-
-    it 'is able k8s core for pod details' do
-      expect(knative.service_pod_details(namespace.namespace, cluster.cluster_project.project.name)).not_to be_nil
-    end
-  end
-
-  describe '#services' do
-    let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
-    let(:service) { cluster.platform_kubernetes }
-    let(:knative) { create(:clusters_applications_knative, cluster: cluster) }
-
-    let(:namespace) do
-      create(:cluster_kubernetes_namespace,
-        cluster: cluster,
-        cluster_project: cluster.cluster_project,
-        project: cluster.cluster_project.project)
-    end
-
-    subject { knative.services }
-
-    before do
-      stub_kubeclient_discover(service.api_url)
-      stub_kubeclient_knative_services
-      stub_kubeclient_service_pods
-    end
-
-    it 'has an unintialized cache' do
-      is_expected.to be_nil
-    end
-
-    context 'when using synchronous reactive cache' do
-      before do
-        stub_reactive_cache(knative,
-          {
-            services: kube_response(kube_knative_services_body),
-            pods: kube_response(kube_knative_pods_body(cluster.cluster_project.project.name, namespace.namespace))
-          })
-        synchronous_reactive_cache(knative)
-      end
-
-      it 'has cached services' do
-        is_expected.not_to be_nil
-      end
-
-      it 'matches our namespace' do
-        expect(knative.services_for(ns: namespace)).not_to be_nil
-      end
-    end
-  end
 end
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index 4739e62289acd8fd8ba183195776f206aef13bdf..f206bb41f45f0106a037e8df792244beb36d3e09 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -38,6 +38,11 @@
 
   it { is_expected.to respond_to :project }
 
+  it do
+    expect(subject.knative_services_finder(subject.project))
+      .to be_instance_of(Clusters::KnativeServicesFinder)
+  end
+
   describe '.enabled' do
     subject { described_class.enabled }
 
diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb
index 78b7ae9c00c84eb940a7b20c519f8c4a6612c321..011c4df0fe5f3ef575e487546809e111b5362597 100644
--- a/spec/support/helpers/kubernetes_helpers.rb
+++ b/spec/support/helpers/kubernetes_helpers.rb
@@ -17,17 +17,38 @@ def kube_deployments_response
     kube_response(kube_deployments_body)
   end
 
-  def stub_kubeclient_discover(api_url)
+  def stub_kubeclient_discover_base(api_url)
     WebMock.stub_request(:get, api_url + '/api/v1').to_return(kube_response(kube_v1_discovery_body))
-    WebMock.stub_request(:get, api_url + '/apis/extensions/v1beta1').to_return(kube_response(kube_v1beta1_discovery_body))
-    WebMock.stub_request(:get, api_url + '/apis/rbac.authorization.k8s.io/v1').to_return(kube_response(kube_v1_rbac_authorization_discovery_body))
-    WebMock.stub_request(:get, api_url + '/apis/serving.knative.dev/v1alpha1').to_return(kube_response(kube_v1alpha1_serving_knative_discovery_body))
+    WebMock
+      .stub_request(:get, api_url + '/apis/extensions/v1beta1')
+      .to_return(kube_response(kube_v1beta1_discovery_body))
+    WebMock
+      .stub_request(:get, api_url + '/apis/rbac.authorization.k8s.io/v1')
+      .to_return(kube_response(kube_v1_rbac_authorization_discovery_body))
+  end
+
+  def stub_kubeclient_discover(api_url)
+    stub_kubeclient_discover_base(api_url)
+
+    WebMock
+      .stub_request(:get, api_url + '/apis/serving.knative.dev/v1alpha1')
+      .to_return(kube_response(kube_v1alpha1_serving_knative_discovery_body))
+  end
+
+  def stub_kubeclient_discover_knative_not_found(api_url)
+    stub_kubeclient_discover_base(api_url)
+
+    WebMock
+      .stub_request(:get, api_url + '/apis/serving.knative.dev/v1alpha1')
+      .to_return(status: [404, "Resource Not Found"])
   end
 
-  def stub_kubeclient_service_pods(status: nil)
+  def stub_kubeclient_service_pods(response = nil, options = {})
     stub_kubeclient_discover(service.api_url)
-    pods_url = service.api_url + "/api/v1/pods"
-    response = { status: status } if status
+
+    namespace_path = options[:namespace].present? ? "namespaces/#{options[:namespace]}/" : ""
+
+    pods_url = service.api_url + "/api/v1/#{namespace_path}pods"
 
     WebMock.stub_request(:get, pods_url).to_return(response || kube_pods_response)
   end
@@ -56,15 +77,18 @@ def stub_kubeclient_deployments(namespace, status: nil)
     WebMock.stub_request(:get, deployments_url).to_return(response || kube_deployments_response)
   end
 
-  def stub_kubeclient_knative_services(**options)
+  def stub_kubeclient_knative_services(options = {})
+    namespace_path = options[:namespace].present? ? "namespaces/#{options[:namespace]}/" : ""
+
     options[:name] ||= "kubetest"
-    options[:namespace] ||= "default"
     options[:domain] ||= "example.com"
+    options[:response] ||= kube_response(kube_knative_services_body(options))
 
     stub_kubeclient_discover(service.api_url)
-    knative_url = service.api_url + "/apis/serving.knative.dev/v1alpha1/services"
 
-    WebMock.stub_request(:get, knative_url).to_return(kube_response(kube_knative_services_body(options)))
+    knative_url = service.api_url + "/apis/serving.knative.dev/v1alpha1/#{namespace_path}services"
+
+    WebMock.stub_request(:get, knative_url).to_return(options[:response])
   end
 
   def stub_kubeclient_get_secret(api_url, **options)