diff --git a/app/assets/stylesheets/page_bundles/ci_status.scss b/app/assets/stylesheets/page_bundles/ci_status.scss
index f2129aa6841695450f549a97a478416546444c6a..6d2849345c0fd457f30f4f53e10fd386be045694 100644
--- a/app/assets/stylesheets/page_bundles/ci_status.scss
+++ b/app/assets/stylesheets/page_bundles/ci_status.scss
@@ -15,17 +15,16 @@
   }
 
   &.ci-failed {
-    @include status-color(
-      var(--red-100, $red-100),
+    @include status-color(var(--red-100, $red-100),
       var(--red-500, $red-500),
-      var(--red-600, $red-600)
-    );
+      var(--red-600, $red-600));
   }
 
   &.ci-success {
     @include green-status-color;
   }
 
+  &.ci-canceling,
   &.ci-canceled,
   &.ci-disabled,
   &.ci-scheduled,
@@ -39,11 +38,9 @@
   }
 
   &.ci-preparing {
-    @include status-color(
-      var(--gray-100, $gray-100),
+    @include status-color(var(--gray-100, $gray-100),
       var(--gray-300, $gray-300),
-      var(--gray-400, $gray-400)
-    );
+      var(--gray-400, $gray-400));
   }
 
   &.ci-pending,
@@ -51,20 +48,16 @@
   &.ci-waiting-for-callback,
   &.ci-failed-with-warnings,
   &.ci-success-with-warnings {
-    @include status-color(
-      var(--orange-50, $orange-50),
+    @include status-color(var(--orange-50, $orange-50),
       var(--orange-500, $orange-500),
-      var(--orange-700, $orange-700)
-    );
+      var(--orange-700, $orange-700));
   }
 
   &.ci-info,
   &.ci-running {
-    @include status-color(
-      var(--blue-100, $blue-100),
+    @include status-color(var(--blue-100, $blue-100),
       var(--blue-500, $blue-500),
-      var(--blue-600, $blue-600)
-    );
+      var(--blue-600, $blue-600));
   }
 
   &.ci-created,
@@ -76,4 +69,4 @@
       background-color: rgba($gl-text-color-secondary, 0.07);
     }
   }
-}
+}
\ No newline at end of file
diff --git a/app/graphql/types/ci/pipeline_status_enum.rb b/app/graphql/types/ci/pipeline_status_enum.rb
index 17cf48bb5cf95cb1a7413d14590cb592495ec0d4..5fa90cf243cee3822df805f7e513a353ce78c737 100644
--- a/app/graphql/types/ci/pipeline_status_enum.rb
+++ b/app/graphql/types/ci/pipeline_status_enum.rb
@@ -12,6 +12,7 @@ class PipelineStatusEnum < BaseEnum
         running: 'Pipeline is running.',
         failed: 'At least one stage of the pipeline failed.',
         success: 'Pipeline completed successfully.',
+        canceling: 'Pipeline is in the process of canceling.',
         canceled: 'Pipeline was canceled before completion.',
         skipped: 'Pipeline was skipped.',
         manual: 'Pipeline needs to be manually started.',
diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb
index d62bdfa4c8701f93236d6e0f9b68f06a1650a7af..0c4b5bd087401c59b9b2e3a0f81af9cade507514 100644
--- a/app/models/ci/bridge.rb
+++ b/app/models/ci/bridge.rb
@@ -37,6 +37,10 @@ class Bridge < Ci::Processable
         end
       end
 
+      event :canceling do
+        transition CANCELABLE_STATUSES.map(&:to_sym) => :canceling
+      end
+
       event :pending do
         transition all => :pending
       end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 515e7966cc3c4af35ec5653354928fc9a42c0403..16d3318b676b0a576369fcc58bec81ee831733c5 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -395,6 +395,12 @@ def self.ids_in_merge_request(merge_request_ids)
       in_merge_request(merge_request_ids).pluck(:id)
     end
 
+    # A Ci::Bridge may transition to `canceling` as a result of strategy: :depend
+    # but only a Ci::Build will transition to `canceling`` via `.cancel`
+    def supports_canceling?
+      Feature.enabled?(:ci_canceling_status, project, type: :wip) && cancel_gracefully?
+    end
+
     def build_matcher
       strong_memoize(:build_matcher) do
         Gitlab::Ci::Matching::BuildMatcher.new({
@@ -472,7 +478,7 @@ def play(current_user, job_variables_attributes = nil)
     # rubocop: enable CodeReuse/ServiceClass
 
     def cancelable?
-      active? || created?
+      (active? || created?) && !canceling?
     end
 
     def retries_count
diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb
index 1fe6af8c595ca3cf14c706dee685a2000d5cf738..73616c4043ad3467563978bde843fb4bc7c92329 100644
--- a/app/models/ci/build_metadata.rb
+++ b/app/models/ci/build_metadata.rb
@@ -64,7 +64,7 @@ def update_timeout_state
     end
 
     def set_cancel_gracefully
-      runtime_runner_features.merge!({ cancel_gracefully: true })
+      runtime_runner_features[:cancel_gracefully] = true
     end
 
     def cancel_gracefully?
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 2802072d937020443bb3dbedcc4251fbfaee79fa..057a2725aa68e843300c93bdded626160e7d5ccc 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -187,7 +187,7 @@ class Pipeline < Ci::ApplicationRecord
     state_machine :status, initial: :created do
       event :enqueue do
         transition [:created, :manual, :waiting_for_resource, :preparing, :skipped, :scheduled] => :pending
-        transition [:success, :failed, :canceled] => :running
+        transition [:success, :failed, :canceling, :canceled] => :running
 
         # this is needed to ensure tests to be covered
         transition [:running] => :running
@@ -226,6 +226,10 @@ class Pipeline < Ci::ApplicationRecord
         transition any => :success
       end
 
+      event :canceling do
+        transition any - [:canceling, :canceled] => :canceling
+      end
+
       event :cancel do
         transition any - [:canceled] => :canceled
       end
@@ -866,6 +870,7 @@ def notes
       project.notes.for_commit_id(sha)
     end
 
+    # rubocop: disable Metrics/CyclomaticComplexity -- breaking apart hurts readability
     def set_status(new_status)
       retry_optimistic_lock(self, name: 'ci_pipeline_set_status') do
         case new_status
@@ -877,6 +882,7 @@ def set_status(new_status)
         when 'running' then run
         when 'success' then succeed
         when 'failed' then drop
+        when 'canceling' then canceling
         when 'canceled' then cancel
         when 'skipped' then skip
         when 'manual' then block
@@ -886,6 +892,7 @@ def set_status(new_status)
         end
       end
     end
+    # rubocop: enable Metrics/CyclomaticComplexity
 
     def protected_ref?
       strong_memoize(:protected_ref) { project.protected_for?(git_ref) }
diff --git a/app/models/ci/processable.rb b/app/models/ci/processable.rb
index 989d6337ab7600569895c9ef34a836db73af735c..8e4784e3f38f53eaeacca388116b5f0a9549b22f 100644
--- a/app/models/ci/processable.rb
+++ b/app/models/ci/processable.rb
@@ -137,7 +137,7 @@ def clone(current_user:, new_job_variables_attributes: [])
     def retryable?
       return false if retried? || archived? || deployment_rejected?
 
-      success? || failed? || canceled?
+      success? || failed? || canceled? || canceling?
     end
 
     def aggregated_needs_names
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index 251a3089667b68f6455f6a84719dbbeac3ae0f74..8743ed5b00929a8aad2b1e50ed32938397399182 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -117,6 +117,10 @@ class Stage < Ci::ApplicationRecord
         transition any - [:success] => :success
       end
 
+      event :canceling do
+        transition any - [:canceling, :canceled] => :canceling
+      end
+
       event :cancel do
         transition any - [:canceled] => :canceled
       end
@@ -134,6 +138,7 @@ def self.use_partition_id_filter?
       Ci::Pipeline.use_partition_id_filter?
     end
 
+    # rubocop: disable Metrics/CyclomaticComplexity -- breaking apart hurts readability, consider refactoring issue #439268
     def set_status(new_status)
       retry_optimistic_lock(self, name: 'ci_stage_set_status') do
         case new_status
@@ -145,6 +150,7 @@ def set_status(new_status)
         when 'running' then run
         when 'success' then succeed
         when 'failed' then drop
+        when 'canceling' then canceling
         when 'canceled' then cancel
         when 'manual' then block
         when 'scheduled' then delay
@@ -154,6 +160,7 @@ def set_status(new_status)
         end
       end
     end
+    # rubocop: enable Metrics/CyclomaticComplexity
 
     # This will be removed with ci_remove_ensure_stage_service
     def update_legacy_status
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 7ec3210f703ebad06366e8875ed85cbc9a648699..386ff3a45849704af0898eaaf08130abd10e2273 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -165,15 +165,18 @@ class CommitStatus < Ci::ApplicationRecord
     end
 
     event :drop do
+      transition canceling: :canceled # runner returns success/failed
       transition [:created, :waiting_for_resource, :preparing, :waiting_for_callback, :pending, :running, :manual, :scheduled] => :failed
     end
 
     event :success do
+      transition canceling: :canceled # runner returns success/failed
       transition [:created, :waiting_for_resource, :preparing, :waiting_for_callback, :pending, :running] => :success
     end
 
     event :cancel do
-      transition [:created, :waiting_for_resource, :preparing, :waiting_for_callback, :pending, :running, :manual, :scheduled] => :canceled
+      transition running: :canceling, if: :supports_canceling?
+      transition [:created, :waiting_for_resource, :preparing, :waiting_for_callback, :pending, :manual, :scheduled, :running] => :canceled
     end
 
     before_transition [:created, :waiting_for_resource, :preparing, :skipped, :manual, :scheduled] => :pending do |commit_status|
@@ -258,6 +261,10 @@ def group_name
     name.to_s.sub(regex, '').strip
   end
 
+  def supports_canceling?
+    false
+  end
+
   # Time spent running.
   def duration
     calculate_duration(started_at, finished_at)
diff --git a/app/models/concerns/ci/has_status.rb b/app/models/concerns/ci/has_status.rb
index cf7442d186362cfc2e420a5b992a6d3585d385bd..468e66e4245b1aab2bf08475cc1e5d3311d05044 100644
--- a/app/models/concerns/ci/has_status.rb
+++ b/app/models/concerns/ci/has_status.rb
@@ -6,20 +6,20 @@ module HasStatus
 
     DEFAULT_STATUS = 'created'
     BLOCKED_STATUS = %w[manual scheduled].freeze
-    AVAILABLE_STATUSES = %w[created waiting_for_resource preparing waiting_for_callback pending running success failed canceled skipped manual scheduled].freeze
+    AVAILABLE_STATUSES = %w[created waiting_for_resource preparing waiting_for_callback pending running success failed canceling canceled skipped manual scheduled].freeze
     STARTED_STATUSES = %w[running success failed].freeze
     ACTIVE_STATUSES = %w[waiting_for_resource preparing waiting_for_callback pending running].freeze
     COMPLETED_STATUSES = %w[success failed canceled skipped].freeze
     STOPPED_STATUSES = COMPLETED_STATUSES + BLOCKED_STATUS
-    ORDERED_STATUSES = %w[failed preparing pending running waiting_for_callback waiting_for_resource manual scheduled canceled success skipped created].freeze
+    ORDERED_STATUSES = %w[failed preparing pending running waiting_for_callback waiting_for_resource manual scheduled canceling canceled success skipped created].freeze
     PASSED_WITH_WARNINGS_STATUSES = %w[failed canceled].to_set.freeze
     IGNORED_STATUSES = %w[manual].to_set.freeze
-    ALIVE_STATUSES = (ACTIVE_STATUSES + ['created']).freeze
-    CANCELABLE_STATUSES = (ALIVE_STATUSES + ['scheduled']).freeze
+    ALIVE_STATUSES = ORDERED_STATUSES - COMPLETED_STATUSES - BLOCKED_STATUS
+    CANCELABLE_STATUSES = (ALIVE_STATUSES + ['scheduled'] - ['canceling']).freeze
     STATUSES_ENUM = { created: 0, pending: 1, running: 2, success: 3,
                       failed: 4, canceled: 5, skipped: 6, manual: 7,
                       scheduled: 8, preparing: 9, waiting_for_resource: 10,
-                      waiting_for_callback: 11 }.freeze
+                      waiting_for_callback: 11, canceling: 12 }.freeze
 
     UnknownStatusError = Class.new(StandardError)
 
@@ -69,6 +69,7 @@ def stopped_statuses
         state :failed, value: 'failed'
         state :success, value: 'success'
         state :canceled, value: 'canceled'
+        state :canceling, value: 'canceling'
         state :skipped, value: 'skipped'
         state :manual, value: 'manual'
         state :scheduled, value: 'scheduled'
@@ -83,6 +84,7 @@ def stopped_statuses
       scope :pending, -> { with_status(:pending) }
       scope :success, -> { with_status(:success) }
       scope :failed, -> { with_status(:failed) }
+      scope :canceling, -> { with_status(:canceling) }
       scope :canceled, -> { with_status(:canceled) }
       scope :skipped, -> { with_status(:skipped) }
       scope :manual, -> { with_status(:manual) }
@@ -92,7 +94,7 @@ def stopped_statuses
       scope :created_or_pending, -> { with_status(:created, :pending) }
       scope :running_or_pending, -> { with_status(:running, :pending) }
       scope :finished, -> { with_status(:success, :failed, :canceled) }
-      scope :failed_or_canceled, -> { with_status(:failed, :canceled) }
+      scope :failed_or_canceled, -> { with_status(:failed, :canceled, :canceling) }
       scope :complete, -> { with_status(completed_statuses) }
       scope :incomplete, -> { without_statuses(completed_statuses) }
       scope :waiting_for_resource_or_upcoming, -> { with_status(:created, :scheduled, :waiting_for_resource) }
diff --git a/config/feature_flags/wip/ci_canceling_status.yml b/config/feature_flags/wip/ci_canceling_status.yml
new file mode 100644
index 0000000000000000000000000000000000000000..41e0c95b5e566af031329c74185ebcca7695409d
--- /dev/null
+++ b/config/feature_flags/wip/ci_canceling_status.yml
@@ -0,0 +1,9 @@
+---
+name: ci_canceling_status
+feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/399215
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/140522
+rollout_issue_url: https://gitlab.com/gitlab-com/gl-infra/production/-/issues/17520
+milestone: '16.10'
+group: group::pipeline execution
+type: wip
+default_enabled: false
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index dae84988792e5a00a2af83f0943297498adeeb98..b4c29f41922d493deee38303d552908ba2d7435d 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -24776,7 +24776,7 @@ Returns [`UserMergeRequestInteraction`](#usermergerequestinteraction).
 | <a id="pipelinesourcejob"></a>`sourceJob` | [`CiJob`](#cijob) | Job where pipeline was triggered from. |
 | <a id="pipelinestages"></a>`stages` | [`CiStageConnection`](#cistageconnection) | Stages of the pipeline. (see [Connections](#connections)) |
 | <a id="pipelinestartedat"></a>`startedAt` | [`Time`](#time) | Timestamp when the pipeline was started. |
-| <a id="pipelinestatus"></a>`status` | [`PipelineStatusEnum!`](#pipelinestatusenum) | Status of the pipeline (CREATED, WAITING_FOR_RESOURCE, PREPARING, WAITING_FOR_CALLBACK, PENDING, RUNNING, FAILED, SUCCESS, CANCELED, SKIPPED, MANUAL, SCHEDULED). |
+| <a id="pipelinestatus"></a>`status` | [`PipelineStatusEnum!`](#pipelinestatusenum) | Status of the pipeline (CREATED, WAITING_FOR_RESOURCE, PREPARING, WAITING_FOR_CALLBACK, PENDING, RUNNING, FAILED, SUCCESS, CANCELED, CANCELING, SKIPPED, MANUAL, SCHEDULED). |
 | <a id="pipelinestuck"></a>`stuck` | [`Boolean!`](#boolean) | If the pipeline is stuck. |
 | <a id="pipelinetestreportsummary"></a>`testReportSummary` | [`TestReportSummary!`](#testreportsummary) | Summary of the test report generated by the pipeline. |
 | <a id="pipelinetotaljobs"></a>`totalJobs` | [`Int!`](#int) | The total number of jobs in the pipeline. |
@@ -30725,6 +30725,7 @@ Values for sorting inherited variables.
 | Value | Description |
 | ----- | ----------- |
 | <a id="cijobstatuscanceled"></a>`CANCELED` | A job that is canceled. |
+| <a id="cijobstatuscanceling"></a>`CANCELING` | A job that is canceling. |
 | <a id="cijobstatuscreated"></a>`CREATED` | A job that is created. |
 | <a id="cijobstatusfailed"></a>`FAILED` | A job that is failed. |
 | <a id="cijobstatusmanual"></a>`MANUAL` | A job that is manual. |
@@ -32402,6 +32403,7 @@ Pipeline security report finding sort values.
 | Value | Description |
 | ----- | ----------- |
 | <a id="pipelinestatusenumcanceled"></a>`CANCELED` | Pipeline was canceled before completion. |
+| <a id="pipelinestatusenumcanceling"></a>`CANCELING` | Pipeline is in the process of canceling. |
 | <a id="pipelinestatusenumcreated"></a>`CREATED` | Pipeline has been created. |
 | <a id="pipelinestatusenumfailed"></a>`FAILED` | At least one stage of the pipeline failed. |
 | <a id="pipelinestatusenummanual"></a>`MANUAL` | Pipeline needs to be manually started. |
diff --git a/ee/app/models/ee/ci/bridge.rb b/ee/app/models/ee/ci/bridge.rb
index 6d85b5f5576c2d51866cb41cc3e9b70b92328f36..8bc88610fba2a569bd7ed4d51686922f64005e72 100644
--- a/ee/app/models/ee/ci/bridge.rb
+++ b/ee/app/models/ee/ci/bridge.rb
@@ -36,6 +36,8 @@ def inherit_status_from_upstream!
           self.success!
         when 'failed'
           self.drop!
+        when 'canceling'
+          self.canceling!
         when 'canceled'
           self.cancel!
         when 'skipped'
diff --git a/ee/spec/models/ci/build_spec.rb b/ee/spec/models/ci/build_spec.rb
index 8a26075b84979b1241f4950dd8e96d3ac774719f..3b546ebf144ac6f070eb452b3cc3a0ef35cf9c16 100644
--- a/ee/spec/models/ci/build_spec.rb
+++ b/ee/spec/models/ci/build_spec.rb
@@ -96,12 +96,31 @@
   describe 'updates pipeline minutes' do
     let(:job) { create(:ci_build, :running, pipeline: pipeline) }
 
-    %w[success drop cancel].each do |event|
-      it "for event #{event}" do
-        expect(Ci::Minutes::UpdateBuildMinutesService)
-          .to receive(:new).and_call_original
+    context 'when ci_canceling_status is disabled' do
+      before do
+        stub_feature_flags(ci_canceling_status: false)
+      end
+
+      %w[success drop cancel].each do |event|
+        it "for event #{event}" do
+          expect(Ci::Minutes::UpdateBuildMinutesService)
+            .to receive(:new).and_call_original
 
-        job.public_send(event)
+          job.public_send(event)
+        end
+      end
+    end
+
+    # TODO: ensure minutes are still tracked when set to
+    # canceled but not when transitioning to canceling
+    context 'when ci_canceling_status is enabled' do
+      %w[success drop].each do |event|
+        it "for event #{event}" do
+          expect(Ci::Minutes::UpdateBuildMinutesService)
+            .to receive(:new).and_call_original
+
+          job.public_send(event)
+        end
       end
     end
   end
diff --git a/ee/spec/models/merge_trains/car_spec.rb b/ee/spec/models/merge_trains/car_spec.rb
index 2b08866a2dbe3c04ec42ea6deee61a6658bfb9fd..50646b483d2da9df135ea04979130b2be2c4df84 100644
--- a/ee/spec/models/merge_trains/car_spec.rb
+++ b/ee/spec/models/merge_trains/car_spec.rb
@@ -450,10 +450,20 @@
     context 'when merge train has a pipeline' do
       let(:train_car) { create(:merge_train_car, pipeline: pipeline) }
       let(:pipeline) { create(:ci_pipeline, :running) }
-      let(:build) { create(:ci_build, :running, pipeline: pipeline) }
+      let(:job) { create(:ci_build, :running, pipeline: pipeline) }
 
-      it 'cancels the jobs in the pipeline' do
-        expect { subject }.to change { build.reload.status }.from('running').to('canceled')
+      context 'when canceling is not supported' do
+        it 'cancels the jobs in the pipeline' do
+          expect { subject }.to change { job.reload.status }.from('running').to('canceled')
+        end
+      end
+
+      context 'when canceling is supported' do
+        include_context 'when canceling support'
+
+        it 'cancels the jobs in the pipeline' do
+          expect { subject }.to change { job.reload.status }.from('running').to('canceling')
+        end
       end
     end
   end
diff --git a/ee/spec/services/auto_merge/merge_train_service_spec.rb b/ee/spec/services/auto_merge/merge_train_service_spec.rb
index bd9d3dc356ffd3bf6aef68b555f48ca16586551f..0f2182104176554fa3b88bd311475522ca43962e 100644
--- a/ee/spec/services/auto_merge/merge_train_service_spec.rb
+++ b/ee/spec/services/auto_merge/merge_train_service_spec.rb
@@ -171,10 +171,28 @@
       end
 
       let(:pipeline) { create(:ci_pipeline) }
-      let(:build) { create(:ci_build, :running, pipeline: pipeline) }
+      let(:job) { create(:ci_build, :running, pipeline: pipeline) }
 
-      it 'cancels the jobs in the pipeline' do
-        expect { subject }.to change { build.reload.status }.from('running').to('canceled')
+      context 'when ci_canceling_status is disabled' do
+        before do
+          stub_feature_flags(ci_canceling_status: false)
+        end
+
+        it 'cancels the jobs in the pipeline' do
+          expect { subject }.to change { job.reload.status }.from('running').to('canceled')
+        end
+      end
+
+      it 'sets the job to a canceled status' do
+        expect { subject }.to change { job.reload.status }.from('running').to('canceled')
+      end
+
+      context 'when canceling is supported' do
+        include_context 'when canceling support'
+
+        it 'sets the job to a canceling status' do
+          expect { subject }.to change { job.reload.status }.from('running').to('canceling')
+        end
       end
     end
 
diff --git a/lib/api/ci/helpers/runner.rb b/lib/api/ci/helpers/runner.rb
index 02a0e6bd7221608af12641ead8ae8bd1df9c20ef..0255a457223c91c8c3caef065fd93a4ecca04c2e 100644
--- a/lib/api/ci/helpers/runner.rb
+++ b/lib/api/ci/helpers/runner.rb
@@ -82,7 +82,7 @@ def authenticate_job!(heartbeat_runner: false)
 
           forbidden!('Project has been deleted!') if job.project.nil? || job.project.pending_delete?
           forbidden!('Job has been erased!') if job.erased?
-          job_forbidden!(job, 'Job is not running') unless job.running?
+          job_forbidden!(job, 'Job is not processing on runner') unless processing_on_runner?(job)
 
           # Only some requests (like updating the job or patching the trace) should trigger
           # runner heartbeat. Operations like artifacts uploading are executed in context of
@@ -152,6 +152,10 @@ def check_if_backoff_required!
 
         private
 
+        def processing_on_runner?(job)
+          job.running? || job.canceling?
+        end
+
         def get_runner_config_from_request
           { config: attributes_for_keys(%w[gpus], params.dig('info', 'config')) }
         end
diff --git a/lib/gitlab/ci/status/build/canceling.rb b/lib/gitlab/ci/status/build/canceling.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4936445cf55308a1bf3be487315ec95e9e1d8b2c
--- /dev/null
+++ b/lib/gitlab/ci/status/build/canceling.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module Ci
+    module Status
+      module Build
+        class Canceling < Status::Extended
+          def illustration
+            {
+              image: 'illustrations/canceled-job_empty.svg',
+              size: '',
+              title: _('This job is in the process of canceling')
+            }
+          end
+
+          def self.matches?(build, _user)
+            build.canceling?
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/ci/status/build/factory.rb b/lib/gitlab/ci/status/build/factory.rb
index 54f6784b847b6003df646ad74a8baf8d281cfab3..5272421a97939865d93bc02c4164427ed441421d 100644
--- a/lib/gitlab/ci/status/build/factory.rb
+++ b/lib/gitlab/ci/status/build/factory.rb
@@ -9,6 +9,7 @@ def self.extended_statuses
             [[Status::Build::Erased,
               Status::Build::Scheduled,
               Status::Build::Manual,
+              Status::Build::Canceling,
               Status::Build::Canceled,
               Status::Build::Created,
               Status::Build::Preparing,
diff --git a/lib/gitlab/ci/status/canceling.rb b/lib/gitlab/ci/status/canceling.rb
new file mode 100644
index 0000000000000000000000000000000000000000..586bac0310ea0bb89f59343f8f40e669d0dd707b
--- /dev/null
+++ b/lib/gitlab/ci/status/canceling.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module Ci
+    module Status
+      class Canceling < Status::Core
+        def text
+          s_('CiStatusText|Canceling')
+        end
+
+        def label
+          s_('CiStatusLabel|canceling')
+        end
+
+        def icon
+          'status_canceled'
+        end
+
+        def favicon
+          'favicon_status_canceled'
+        end
+
+        def details_path
+          nil
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/ci/status/composite.rb b/lib/gitlab/ci/status/composite.rb
index fe4f6db95490834db57c050a203d2e695e69c4bd..c3882c69cc8b5050234dec3aae98e8c4e85e7135 100644
--- a/lib/gitlab/ci/status/composite.rb
+++ b/lib/gitlab/ci/status/composite.rb
@@ -71,6 +71,8 @@ def status
               'preparing'
             elsif any_of?(:created)
               'running'
+            elsif any_of?(:canceling)
+              'canceling'
             else
               'failed'
             end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 5433d9dc4808d37ad8331216da661e363fa4e30f..f3d3220f70ba6221bfa4fe7cbefa50f35bbf6de5 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -10605,6 +10605,9 @@ msgstr ""
 msgid "CiStatusLabel|canceled"
 msgstr ""
 
+msgid "CiStatusLabel|canceling"
+msgstr ""
+
 msgid "CiStatusLabel|created"
 msgstr ""
 
@@ -10653,6 +10656,9 @@ msgstr ""
 msgid "CiStatusText|Canceled"
 msgstr ""
 
+msgid "CiStatusText|Canceling"
+msgstr ""
+
 msgid "CiStatusText|Created"
 msgstr ""
 
@@ -51195,6 +51201,9 @@ msgstr ""
 msgid "This job is in pending state and is waiting to be picked by a runner"
 msgstr ""
 
+msgid "This job is in the process of canceling"
+msgstr ""
+
 msgid "This job is performing tasks that must complete before it can start"
 msgstr ""
 
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index 82c1aa3e18c7d39d37a92af3a23c19413264f7e3..cbc854c7856b833d649afede9fce74c431ff6dea 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -1014,24 +1014,55 @@ def post_request
 
   describe 'POST cancel.json' do
     let!(:pipeline) { create(:ci_pipeline, project: project) }
-    let!(:build) { create(:ci_build, :running, pipeline: pipeline) }
+    let!(:job) { create(:ci_build, :running, pipeline: pipeline) }
 
-    before do
+    subject do
       post :cancel, params: {
         namespace_id: project.namespace, project_id: project, id: pipeline.id
       }, format: :json
     end
 
-    it 'cancels a pipeline without returning any content', :sidekiq_might_not_need_inline do
-      expect(response).to have_gitlab_http_status(:no_content)
-      expect(pipeline.reload).to be_canceled
+    context 'when supports canceling is true' do
+      include_context 'when canceling support'
+
+      it 'sets a pipeline status to canceling', :sidekiq_inline do
+        subject
+
+        expect(pipeline.reload).to be_canceling
+      end
+
+      it 'returns a no content http status' do
+        subject
+
+        expect(response).to have_gitlab_http_status(:no_content)
+      end
     end
 
-    context 'when builds are disabled' do
-      let(:feature) { ProjectFeature::DISABLED }
+    context 'when supports canceling is false' do
+      before do
+        allow(job).to receive(:supports_canceling?).and_return(false)
+      end
 
-      it 'fails to retry pipeline' do
-        expect(response).to have_gitlab_http_status(:not_found)
+      it 'sets a pipeline status to canceled', :sidekiq_inline do
+        subject
+
+        expect(pipeline.reload).to be_canceled
+      end
+
+      it 'returns a no content http status' do
+        subject
+
+        expect(response).to have_gitlab_http_status(:no_content)
+      end
+
+      context 'when builds are disabled' do
+        let(:feature) { ProjectFeature::DISABLED }
+
+        it 'fails to retry pipeline' do
+          subject
+
+          expect(response).to have_gitlab_http_status(:not_found)
+        end
       end
     end
   end
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index 3bfd15b56e02f9ded1c88204e31778014a48d625..0556b42aa97e21fd3fc6f132e35286c6165b527d 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -102,6 +102,11 @@
       status { 'failed' }
     end
 
+    trait :canceling do
+      started
+      status { 'canceling' }
+    end
+
     trait :canceled do
       finished
       status { 'canceled' }
diff --git a/spec/features/projects/jobs/user_browses_jobs_spec.rb b/spec/features/projects/jobs/user_browses_jobs_spec.rb
index 545bfee4910db42eb6259477924084f54680dcb7..bfa845906d8c10918a5ba213021e949985847fce 100644
--- a/spec/features/projects/jobs/user_browses_jobs_spec.rb
+++ b/spec/features/projects/jobs/user_browses_jobs_spec.rb
@@ -69,13 +69,32 @@ def visit_jobs_page
           visit_jobs_page
         end
 
-        it 'cancels a job successfully' do
-          find_by_testid('cancel-button').click
+        context 'when supports canceling is true' do
+          include_context 'when canceling support'
 
-          wait_for_requests
+          it 'cancels a job successfully' do
+            find_by_testid('cancel-button').click
+
+            wait_for_requests
+
+            expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Canceling')
+            expect(page).not_to have_selector('[data-testid="jobs-table-error-alert"]')
+          end
+        end
 
-          expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Canceled')
-          expect(page).not_to have_selector('[data-testid="jobs-table-error-alert"]')
+        context 'when supports canceling is false' do
+          before do
+            stub_feature_flags(ci_canceling_status: false)
+          end
+
+          it 'cancels a job successfully' do
+            find_by_testid('cancel-button').click
+
+            wait_for_requests
+
+            expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Canceled')
+            expect(page).not_to have_selector('[data-testid="jobs-table-error-alert"]')
+          end
         end
       end
 
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index 7de9b3ef7dba46547334e1fd4bffc5865d2fa1fa..4e41fff733d3492355262630569ac4df311640c1 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -193,11 +193,25 @@
           end
         end
 
-        it 'cancels the preparing build and shows retry button', :sidekiq_might_not_need_inline do
+        it 'does not show the retry button' do
           find('#ci-badge-deploy .ci-action-icon-container').click
 
           page.within('#ci-badge-deploy') do
-            expect(page).to have_css('.js-icon-retry')
+            expect(page).not_to have_css('.js-icon-retry')
+          end
+        end
+
+        context 'when ci_canceling_status is disabled' do
+          before do
+            stub_feature_flags(ci_canceling_status: false)
+          end
+
+          it 'shows retry button', :sidekiq_inline do
+            find('#ci-badge-deploy .ci-action-icon-container').click
+
+            page.within('#ci-badge-deploy') do
+              expect(page).to have_css('.js-icon-retry')
+            end
           end
         end
       end
@@ -371,15 +385,25 @@
               expect(page).to have_selector('button[aria-label="Cancel downstream pipeline"]')
             end
 
-            context 'when canceling', :sidekiq_inline do
+            context 'when cancel button clicked', :sidekiq_inline do
               before do
                 find('button[aria-label="Cancel downstream pipeline"]').click
-                wait_for_requests
               end
 
-              it 'shows the pipeline as canceled with the retry action' do
-                expect(page).to have_selector('button[aria-label="Retry downstream pipeline"]')
+              it 'shows the pipeline as canceling with the retry action' do
                 expect(page).to have_selector('[data-testid="status_canceled_borderless-icon"]')
+                expect(page).to have_selector('button[aria-label="Retry downstream pipeline"]')
+              end
+
+              context 'when ci_canceling_status is disabled' do
+                before do
+                  stub_feature_flags(ci_canceling_status: false)
+                end
+
+                it 'shows the pipeline as canceled with the retry action' do
+                  expect(page).to have_selector('[data-testid="status_canceled_borderless-icon"]')
+                  expect(page).to have_selector('button[aria-label="Retry downstream pipeline"]')
+                end
               end
             end
           end
@@ -387,19 +411,25 @@
           context 'with a failed downstream' do
             let(:status) { :failed }
 
-            it 'indicates that pipeline can be retried' do
-              expect(page).to have_selector('button[aria-label="Retry downstream pipeline"]')
-            end
-
-            context 'when retrying' do
+            context 'when ci_canceling_status is disabled' do
               before do
-                find('button[aria-label="Retry downstream pipeline"]').click
-                wait_for_requests
+                stub_feature_flags(ci_canceling_status: false)
               end
 
-              it 'shows running pipeline with the cancel action' do
-                expect(page).to have_selector('[data-testid="status_running_borderless-icon"]')
-                expect(page).to have_selector('button[aria-label="Cancel downstream pipeline"]')
+              it 'indicates that pipeline can be retried' do
+                expect(page).to have_selector('button[aria-label="Retry downstream pipeline"]')
+              end
+
+              context 'when retrying' do
+                before do
+                  find('button[aria-label="Retry downstream pipeline"]').click
+                  wait_for_requests
+                end
+
+                it 'shows running pipeline with the cancel action' do
+                  expect(page).to have_selector('[data-testid="status_running_borderless-icon"]')
+                  expect(page).to have_selector('button[aria-label="Cancel downstream pipeline"]')
+                end
               end
             end
           end
@@ -546,16 +576,19 @@
     context 'canceling jobs' do
       before do
         visit_pipeline
+        click_on 'Cancel pipeline'
       end
 
-      it { expect(page).not_to have_selector('.ci-canceled') }
+      it 'does not show a "Cancel pipeline" button', :sidekiq_inline do
+        expect(page).not_to have_content('Cancel pipeline')
+      end
 
-      context 'when canceling' do
+      context 'when ci_canceling_status disabled' do
         before do
-          click_on 'Cancel pipeline'
+          stub_feature_flags(ci_canceling_status: false)
         end
 
-        it 'does not show a "Cancel pipeline" button', :sidekiq_might_not_need_inline do
+        it 'does not show a "Cancel pipeline" button', :sidekiq_inline do
           expect(page).not_to have_content('Cancel pipeline')
         end
       end
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index 629185e3ba180a63e2500e5f9fac3ed802354eeb..b9c43eb8445035e5052c73f3ec5e45b26a09f72c 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -104,30 +104,58 @@
       end
 
       context 'when pipeline is cancelable' do
-        let!(:build) do
+        let!(:job) do
           create(:ci_build, pipeline: pipeline, stage: 'test')
         end
 
         before do
-          build.run
+          job.run
           visit_project_pipelines
         end
 
-        it 'indicates that pipeline can be canceled' do
-          expect(page).to have_selector('.js-pipelines-cancel-button')
-          expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Running')
+        context 'when canceling support is disabled' do
+          before do
+            stub_feature_flags(ci_canceling_status: false)
+          end
+
+          it 'indicates that pipeline can be canceled' do
+            expect(page).to have_selector('.js-pipelines-cancel-button')
+            expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Running')
+          end
+
+          context 'when canceling' do
+            before do
+              find('.js-pipelines-cancel-button').click
+              click_button 'Stop pipeline'
+              wait_for_requests
+            end
+
+            it 'indicates that pipelines was canceled', :sidekiq_inline do
+              expect(page).not_to have_selector('.js-pipelines-cancel-button')
+              expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Canceled')
+            end
+          end
         end
 
-        context 'when canceling' do
-          before do
-            find('.js-pipelines-cancel-button').click
-            click_button 'Stop pipeline'
-            wait_for_requests
+        context 'when canceling support is enabled' do
+          include_context 'when canceling support'
+
+          it 'indicates that pipeline can be canceled' do
+            expect(page).to have_selector('.js-pipelines-cancel-button')
+            expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Running')
           end
 
-          it 'indicated that pipelines was canceled', :sidekiq_might_not_need_inline do
-            expect(page).not_to have_selector('.js-pipelines-cancel-button')
-            expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Canceled')
+          context 'when canceling' do
+            before do
+              find('.js-pipelines-cancel-button').click
+              click_button 'Stop pipeline'
+              wait_for_requests
+            end
+
+            it 'indicates that pipeline is canceling', :sidekiq_inline do
+              expect(page).not_to have_selector('.js-pipelines-cancel-button')
+              expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Canceling')
+            end
           end
         end
       end
diff --git a/spec/helpers/ci/jobs_helper_spec.rb b/spec/helpers/ci/jobs_helper_spec.rb
index 1394f536c7264e9776244d97f3774b8b093bf4c2..315df38bba703ae6151a78829a8d1717819b24f3 100644
--- a/spec/helpers/ci/jobs_helper_spec.rb
+++ b/spec/helpers/ci/jobs_helper_spec.rb
@@ -34,6 +34,7 @@
 
     it 'returns job statuses' do
       expect(helper.job_statuses).to eq({
+        "canceling" => "CANCELING",
         "canceled" => "CANCELED",
         "created" => "CREATED",
         "failed" => "FAILED",
diff --git a/spec/lib/gitlab/ci/status/build/canceled_spec.rb b/spec/lib/gitlab/ci/status/build/canceled_spec.rb
index 519b970ca5e9212a25d1777db6685122b0e74631..30ef586db8dd96e24af9da8044067c6b334c4d6b 100644
--- a/spec/lib/gitlab/ci/status/build/canceled_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/canceled_spec.rb
@@ -2,8 +2,8 @@
 
 require 'spec_helper'
 
-RSpec.describe Gitlab::Ci::Status::Build::Canceled do
-  let(:user) { create(:user) }
+RSpec.describe Gitlab::Ci::Status::Build::Canceled, feature_category: :continuous_integration do
+  let(:user) { build_stubbed(:user) }
 
   subject do
     described_class.new(double('subject'))
@@ -17,7 +17,7 @@
     subject { described_class.matches?(build, user) }
 
     context 'when build is canceled' do
-      let(:build) { create(:ci_build, :canceled) }
+      let(:build) { build_stubbed(:ci_build, :canceled) }
 
       it 'is a correct match' do
         expect(subject).to be true
@@ -25,7 +25,7 @@
     end
 
     context 'when build is not canceled' do
-      let(:build) { create(:ci_build) }
+      let(:build) { build_stubbed(:ci_build) }
 
       it 'does not match' do
         expect(subject).to be false
diff --git a/spec/lib/gitlab/ci/status/build/canceling_spec.rb b/spec/lib/gitlab/ci/status/build/canceling_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..617a1a0ccd0396c64aca79fd369fd9021abe46db
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/build/canceling_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Status::Build::Canceling, feature_category: :continuous_integration do
+  let(:user) { build_stubbed(:user) }
+
+  subject(:status_instance) do
+    described_class.new(double)
+  end
+
+  describe '#illustration' do
+    it { expect(status_instance.illustration).to include(:image, :size, :title) }
+  end
+
+  describe '.matches?' do
+    subject(:matches?) { described_class.matches?(build, user) }
+
+    context 'when build is canceled' do
+      let(:build) { build_stubbed(:ci_build, :canceling) }
+
+      it 'is a correct match' do
+        expect(matches?).to be true
+      end
+    end
+
+    context 'when build is not canceled' do
+      let(:build) { build_stubbed(:ci_build) }
+
+      it 'does not match' do
+        expect(matches?).to be false
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/ci/status/canceled_spec.rb b/spec/lib/gitlab/ci/status/canceled_spec.rb
index ddb8b7ecff96f559391f6386edf13cfde3636679..868260ab6fa1722ca346cee19b36edb031d43df7 100644
--- a/spec/lib/gitlab/ci/status/canceled_spec.rb
+++ b/spec/lib/gitlab/ci/status/canceled_spec.rb
@@ -2,7 +2,7 @@
 
 require 'spec_helper'
 
-RSpec.describe Gitlab::Ci::Status::Canceled do
+RSpec.describe Gitlab::Ci::Status::Canceled, feature_category: :continuous_integration do
   subject do
     described_class.new(double('subject'), double('user'))
   end
diff --git a/spec/lib/gitlab/ci/status/canceling_spec.rb b/spec/lib/gitlab/ci/status/canceling_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..577ec72cfa0f67196fd1bf11e659d3781513a7e8
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/canceling_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Status::Canceling, feature_category: :continuous_integration do
+  subject(:status) do
+    described_class.new(double, double)
+  end
+
+  describe '#text' do
+    it { expect(status.text).to eq 'Canceling' }
+  end
+
+  describe '#label' do
+    it { expect(status.label).to eq 'canceling' }
+  end
+
+  describe '#icon' do
+    it { expect(status.icon).to eq 'status_canceled' }
+  end
+
+  describe '#favicon' do
+    it { expect(status.favicon).to eq 'favicon_status_canceled' }
+  end
+
+  describe '#group' do
+    it { expect(status.group).to eq 'canceling' }
+  end
+
+  describe '#details_path' do
+    it { expect(status.details_path).to be_nil }
+  end
+end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 7d1199dfc258e957a87e9b474594f1d2b2733e39..f560b7e5a9d86dac8abebe0bb6cb3f4ec591a161 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -5492,6 +5492,42 @@ def run_job_without_exception
     end
   end
 
+  describe '#supports_canceling?' do
+    let(:job) { create(:ci_build, :running, project: project) }
+
+    context 'when the builds runner does not support canceling' do
+      specify { expect(job.supports_canceling?).to be false }
+
+      context 'when the ci_canceling_status flag is disabled' do
+        before do
+          stub_feature_flags(ci_canceling_status: false)
+        end
+
+        it 'returns false' do
+          expect(job.supports_canceling?).to be false
+        end
+      end
+    end
+
+    context 'when the builds runner supports canceling' do
+      include_context 'when canceling support'
+
+      it 'returns true' do
+        expect(job.supports_canceling?).to be true
+      end
+
+      context 'when the ci_canceling_status flag is disabled' do
+        before do
+          stub_feature_flags(ci_canceling_status: false)
+        end
+
+        it 'returns false' do
+          expect(job.supports_canceling?).to be false
+        end
+      end
+    end
+  end
+
   describe '#runtime_runner_features' do
     subject do
       build.save!
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 5b1754d8946637c1c14ca565906a4db0b5394def..d310ff6c2ef9335dde4db353410ac9c0eaf44d8d 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -283,6 +283,12 @@
   describe '#set_status' do
     let(:pipeline) { build(:ci_empty_pipeline, :created) }
 
+    let(:not_transitionable) do
+      [
+        { from_status: :canceled, to_status: :canceling }
+      ]
+    end
+
     where(:from_status, :to_status) do
       from_status_names = described_class.state_machines[:status].states.map(&:name)
       to_status_names = from_status_names - [:created] # we never want to transition into created
@@ -294,12 +300,12 @@
       it do
         pipeline.status = from_status.to_s
 
-        if from_status != to_status || success_to_success?
+        if (from_status != to_status || success_to_success?) && transitionable?(from_status, to_status)
           expect(pipeline.set_status(to_status.to_s))
             .to eq(true)
         else
           expect(pipeline.set_status(to_status.to_s))
-            .to eq(false), "loopback transitions are not allowed"
+            .to eq(false), 'loopback transitions are not allowed'
         end
       end
 
@@ -308,6 +314,14 @@
       def success_to_success?
         from_status == :success && to_status == :success
       end
+
+      def transitionable?(from, to)
+        not_transitionable.each do |exclusion|
+          return false if from.to_sym == exclusion[:from_status].to_sym && to.to_sym == exclusion[:to_status].to_sym
+        end
+
+        true
+      end
     end
   end
 
@@ -1470,6 +1484,12 @@ def create_build(name, status)
     let(:build_b) { create_build('build2', queued_at: 0) }
     let(:build_c) { create_build('build3', queued_at: 0) }
 
+    describe '#canceling' do
+      it 'transitions to canceling' do
+        expect { pipeline.canceling }.to change { pipeline.status }.from('created').to('canceling')
+      end
+    end
+
     %w[succeed! drop! cancel! skip! block! delay!].each do |action|
       context "when the pipeline received #{action} event" do
         it 'deletes a persistent ref asynchronously' do
@@ -2940,7 +2960,7 @@ def create_pipeline(status, ref, sha)
     subject { described_class.bridgeable_statuses }
 
     it { is_expected.to be_an(Array) }
-    it { is_expected.to contain_exactly('running', 'success', 'failed', 'canceled', 'skipped', 'manual', 'scheduled') }
+    it { is_expected.to contain_exactly('running', 'success', 'failed', 'canceling', 'canceled', 'skipped', 'manual', 'scheduled') }
   end
 
   describe '#status', :sidekiq_inline do
diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb
index 4951f57fe6fb8616591620e30d6d14515ca0a7e2..c214d6a88c3d4d2ac7c8173c81dd9b0be8955472 100644
--- a/spec/models/ci/stage_spec.rb
+++ b/spec/models/ci/stage_spec.rb
@@ -89,11 +89,17 @@
       from_status_names.product(to_status_names)
     end
 
+    let(:not_transitionable) do
+      [
+        { from_status: :canceled, to_status: :canceling }
+      ]
+    end
+
     with_them do
       it do
         stage.status = from_status.to_s
 
-        if from_status != to_status
+        if from_status != to_status && transitionable?(from_status, to_status)
           expect(stage.set_status(to_status.to_s))
             .to eq(true)
         else
@@ -102,6 +108,24 @@
         end
       end
     end
+
+    def transitionable?(from, to)
+      not_transitionable.each do |exclusion|
+        return false if from.to_sym == exclusion[:from_status].to_sym && to.to_sym == exclusion[:to_status].to_sym
+      end
+
+      true
+    end
+  end
+
+  describe '#canceling' do
+    it 'transitions to canceling' do
+      stage = create(:ci_stage, pipeline: pipeline, project: pipeline.project, status: 'running')
+      create(:ci_build, :success, stage_id: stage.id)
+      create(:ci_build, :running, stage_id: stage.id)
+
+      expect { stage.canceling }.to change { stage.status }.from('running').to('canceling')
+    end
   end
 
   describe '#update_status' do
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 7c4917596a0d03fd0108c00f0d0edef0b6c8512f..f8573d4a56291fbbe6a751cee5de2b26769837e6 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -54,6 +54,24 @@ def create_status(**opts)
     it { is_expected.to eq(commit_status.user) }
   end
 
+  describe '#success' do
+    it 'transitions canceling to canceled' do
+      commit_status = create_status(stage: 'test', status: 'canceling')
+
+      expect { commit_status.success! }.to change { commit_status.status }.from('canceling').to('canceled')
+    end
+
+    context 'when status is one that transitions to success' do
+      [:created, :waiting_for_resource, :preparing, :waiting_for_callback, :pending, :running].each do |status|
+        it 'transitions to success' do
+          commit_status = create_status(stage: 'test', status: status.to_s)
+
+          expect { commit_status.success! }.to change { commit_status.status }.from(status.to_s).to('success')
+        end
+      end
+    end
+  end
+
   describe 'status state machine' do
     let!(:commit_status) { create(:commit_status, :running, project: project) }
 
@@ -795,6 +813,23 @@ def create_status(**opts)
         end
       end
     end
+
+    it 'transitions canceling to canceled' do
+      commit_status = create_status(stage: 'test', status: 'canceling')
+
+      expect { commit_status.drop! }.to change { commit_status.status }.from('canceling').to('canceled')
+    end
+
+    context 'when status is one that transitions to success' do
+      [:created, :waiting_for_resource, :preparing, :waiting_for_callback, :pending, :running, :manual,
+:scheduled].each do |status|
+        it 'transitions to success' do
+          commit_status = create_status(stage: 'test', status: status.to_s)
+
+          expect { commit_status.drop! }.to change { commit_status.status }.from(status.to_s).to('failed')
+        end
+      end
+    end
   end
 
   describe 'ensure stage assignment' do
diff --git a/spec/requests/api/ci/pipelines_spec.rb b/spec/requests/api/ci/pipelines_spec.rb
index ef169dbe872406bf35c07fbad0d1dc95b7011e31..ee39a9eb2e6bef18a57efab4d5eadc64b45aff7d 100644
--- a/spec/requests/api/ci/pipelines_spec.rb
+++ b/spec/requests/api/ci/pipelines_spec.rb
@@ -1256,14 +1256,31 @@ def expect_variables(variables, expected_variables)
       create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch)
     end
 
-    let_it_be(:build) { create(:ci_build, :running, pipeline: pipeline) }
+    let_it_be(:job) { create(:ci_build, :running, pipeline: pipeline) }
 
     context 'authorized user', :aggregate_failures do
-      it 'retries failed builds', :sidekiq_might_not_need_inline do
-        post api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", user)
+      context 'when supports canceling is true' do
+        include_context 'when canceling support'
 
-        expect(response).to have_gitlab_http_status(:ok)
-        expect(json_response['status']).to eq('canceled')
+        it 'cancels builds', :sidekiq_inline do
+          post api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", user)
+
+          expect(response).to have_gitlab_http_status(:ok)
+          expect(json_response['status']).to eq('canceling')
+        end
+
+        context 'when ci_canceling_status is disabled' do
+          before do
+            stub_feature_flags(ci_canceling_status: false)
+          end
+
+          it 'cancels builds', :sidekiq_inline do
+            post api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", user)
+
+            expect(response).to have_gitlab_http_status(:ok)
+            expect(json_response['status']).to eq('canceled')
+          end
+        end
       end
     end
 
diff --git a/spec/requests/api/ci/runner/jobs_trace_spec.rb b/spec/requests/api/ci/runner/jobs_trace_spec.rb
index 8c596d2338f0256ea6bda9e7a3e4a9cfd4bc6c97..5c9166f944e970548d8ed4fb53c297f1f3c6f678 100644
--- a/spec/requests/api/ci/runner/jobs_trace_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_trace_spec.rb
@@ -142,17 +142,35 @@
             expect(job.reload.trace.raw).to eq 'BUILD TRACE appended appended'
           end
 
-          context 'when job is cancelled' do
+          context 'when canceling is supported' do
+            include_context 'when canceling support'
+
+            context 'when job is cancelled' do
+              before do
+                job.cancel
+              end
+
+              it 'patching the trace is allowed' do
+                patch_the_trace
+
+                expect(response).to have_gitlab_http_status(:accepted)
+              end
+            end
+          end
+
+          context 'when canceling is not supported' do
             before do
-              job.cancel
+              stub_feature_flags(ci_canceling_status: false)
             end
 
-            context 'when trace is patched' do
+            context 'when job is canceled' do
               before do
-                patch_the_trace
+                job.cancel
               end
 
-              it 'returns Forbidden' do
+              it 'patching the trace returns forbidden' do
+                patch_the_trace
+
                 expect(response).to have_gitlab_http_status(:forbidden)
               end
             end
@@ -203,13 +221,26 @@
           end
         end
 
-        context 'when the job is canceled' do
-          before do
+        context 'when canceling is supported' do
+          include_context 'when canceling support'
+
+          it 'receives status in header' do
             job.cancel
             patch_the_trace
+
+            expect(response.header['Job-Status']).to eq 'canceling'
+          end
+        end
+
+        context 'when canceling is not supported' do
+          before do
+            stub_feature_flags(ci_canceling_status: false)
           end
 
           it 'receives status in header' do
+            job.cancel
+            patch_the_trace
+
             expect(response.header['Job-Status']).to eq 'canceled'
           end
         end
diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_cancel_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_cancel_spec.rb
index 8c1359384ed5a703e8099d630b068b071bdd05eb..be619394b6f79fc10b5c1d2a11f83beaab219859 100644
--- a/spec/requests/api/graphql/mutations/ci/pipeline_cancel_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/pipeline_cancel_spec.rb
@@ -40,13 +40,33 @@
     expect(build).not_to be_canceled
   end
 
-  it 'cancels all cancelable builds from a pipeline', :sidekiq_inline do
-    build = create(:ci_build, :running, pipeline: pipeline)
+  context 'when running build' do
+    let!(:job) { create(:ci_build, :running, pipeline: pipeline) }
 
-    post_graphql_mutation(mutation, current_user: user)
+    context 'when supports canceling is true' do
+      include_context 'when canceling support'
+
+      it 'transitions all running jobs to canceling', :sidekiq_inline do
+        post_graphql_mutation(mutation, current_user: user)
+
+        expect(response).to have_gitlab_http_status(:success)
+        expect(job.reload).to be_canceling
+        expect(pipeline.reload).to be_canceling
+      end
+    end
+
+    context 'when supports canceling is false' do
+      before do
+        stub_feature_flags(ci_canceling_status: false)
+      end
+
+      it 'cancels all running jobs to canceled', :sidekiq_inline do
+        post_graphql_mutation(mutation, current_user: user)
 
-    expect(response).to have_gitlab_http_status(:success)
-    expect(build.reload).to be_canceled
-    expect(pipeline.reload).to be_canceled
+        expect(response).to have_gitlab_http_status(:success)
+        expect(job.reload).to be_canceled
+        expect(pipeline.reload).to be_canceled
+      end
+    end
   end
 end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 8e4e90ae9625d3298233c86e382456044fdef0c2..33c4e6a88d5550321e147320bbb2738a92dd2385 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -306,7 +306,7 @@ def execute_service(
 
           context 'when only interruptible builds are running' do
             context 'when build marked explicitly by interruptible is running' do
-              it 'cancels running outdated pipelines', :sidekiq_might_not_need_inline do
+              it 'cancels running outdated pipelines', :sidekiq_inline do
                 pipeline_on_previous_commit
                   .builds
                   .find_by_name('build_1_2')
@@ -320,7 +320,7 @@ def execute_service(
             end
 
             context 'when build that is not marked as interruptible is running' do
-              it 'cancels running outdated pipelines', :sidekiq_might_not_need_inline do
+              it 'cancels running outdated pipelines', :sidekiq_inline do
                 build_2_1 = pipeline_on_previous_commit
                   .builds.find_by_name('build_2_1')
 
diff --git a/spec/support/shared_examples/ci/jobs_shared_examples.rb b/spec/support/shared_examples/ci/jobs_shared_examples.rb
index d952d4a98eb7fcf261bce86eb7a21622195f5840..a9affa772852efbb53254e5293bf012f208a8064 100644
--- a/spec/support/shared_examples/ci/jobs_shared_examples.rb
+++ b/spec/support/shared_examples/ci/jobs_shared_examples.rb
@@ -25,3 +25,10 @@
     end
   end
 end
+
+RSpec.shared_context 'when canceling support' do
+  before do
+    job.metadata.set_cancel_gracefully
+    job.save!
+  end
+end
diff --git a/spec/workers/ci/cancel_pipeline_worker_spec.rb b/spec/workers/ci/cancel_pipeline_worker_spec.rb
index 8e8f9a78132939c17d68c85d93d3b1d2547a54c7..8ffbd8913bbdfa0735dbdf17340daf59dd884627 100644
--- a/spec/workers/ci/cancel_pipeline_worker_spec.rb
+++ b/spec/workers/ci/cancel_pipeline_worker_spec.rb
@@ -54,7 +54,9 @@
     end
 
     describe 'with builds and state transition side effects', :sidekiq_inline do
-      let!(:build) { create(:ci_build, :running, pipeline: pipeline) }
+      let!(:job) { create(:ci_build, :running, pipeline: pipeline) }
+
+      include_context 'when canceling support'
 
       it_behaves_like 'an idempotent worker', :sidekiq_inline do
         let(:job_args) { [pipeline.id, pipeline.id] }
@@ -64,8 +66,8 @@
 
           pipeline.reload
 
-          expect(pipeline).to be_canceled
-          expect(pipeline.builds.first).to be_canceled
+          expect(pipeline).to be_canceling
+          expect(pipeline.builds.first).to be_canceling
           expect(pipeline.builds.first.auto_canceled_by_id).to eq pipeline.id
           expect(pipeline.auto_canceled_by_id).to eq pipeline.id
         end