diff --git a/CHANGELOG b/CHANGELOG
index dbc54021d40eed1324c4706cfce2866ca6ef5cb3..381369f0dea464559f472f197166d300b64cc335 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -40,6 +40,7 @@ v 7.12.0 (unreleased)
   - Add an option to automatically sign-in with an Omniauth provider
   - Better performance for web editor (switched from satellites to rugged)
   - GitLab CI service sends .gitlab-ci.yaml in each push call
+  - When remove project - move repository and schedule it removal
 
 v 7.11.4
   - Fix missing bullets when creating lists
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index dc4303515510a7767e8ba219ae7db53eedf88e4f..4ca5fc6545902f0d84e28be397cd66157d536bf4 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -97,18 +97,15 @@ class ProjectsController < ApplicationController
     return access_denied! unless can?(current_user, :remove_project, @project)
 
     ::Projects::DestroyService.new(@project, current_user, {}).execute
+    flash[:alert] = 'Project deleted.'
 
-    respond_to do |format|
-      format.html do
-        flash[:alert] = 'Project deleted.'
-
-        if request.referer.include?('/admin')
-          redirect_to admin_namespaces_projects_path
-        else
-          redirect_to dashboard_path
-        end
-      end
+    if request.referer.include?('/admin')
+      redirect_to admin_namespaces_projects_path
+    else
+      redirect_to dashboard_path
     end
+  rescue Projects::DestroyService::DestroyError => ex
+    redirect_to edit_project_path(@project), alert: ex.message
   end
 
   def autocomplete_sources
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index 7e1d753b021d9cdded104c658f7b5d4d0b5abf61..53bf36b101002426195b5ac49b1f48af714a88e5 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -1,28 +1,67 @@
 module Projects
   class DestroyService < BaseService
+    include Gitlab::ShellAdapter
+
+    class DestroyError < StandardError; end
+
+    DELETED_FLAG = '+deleted'
+
     def execute
       return false unless can?(current_user, :remove_project, project)
 
       project.team.truncate
       project.repository.expire_cache unless project.empty_repo?
 
-      if project.destroy
-        GitlabShellWorker.perform_async(
-          :remove_repository,
-          project.path_with_namespace
-        )
+      repo_path = project.path_with_namespace
+      wiki_path = repo_path + '.wiki'
+
+      Project.transaction do
+        project.destroy!
 
-        GitlabShellWorker.perform_async(
-          :remove_repository,
-          project.path_with_namespace + ".wiki"
-        )
+        unless remove_repository(repo_path)
+          raise_error('Failed to remove project repository. Please try again or contact administrator')
+        end
+
+        unless remove_repository(wiki_path)
+          raise_error('Failed to remove wiki repository. Please try again or contact administrator')
+        end
+      end
+
+      project.satellite.destroy
+      log_info("Project \"#{project.name}\" was removed")
+      system_hook_service.execute_hooks_for(project, :destroy)
+      true
+    end
 
-        project.satellite.destroy
+    private
 
-        log_info("Project \"#{project.name}\" was removed")
-        system_hook_service.execute_hooks_for(project, :destroy)
-        true
+    def remove_repository(path)
+      unless gitlab_shell.exists?(path + '.git')
+        return true
       end
+
+      new_path = removal_path(path)
+
+      if gitlab_shell.mv_repository(path, new_path)
+        log_info("Repository \"#{path}\" moved to \"#{new_path}\"")
+        GitlabShellWorker.perform_in(30.seconds, :remove_repository, new_path)
+      else
+        false
+      end
+    end
+
+    def raise_error(message)
+      raise DestroyError.new(message)
+    end
+
+    # Build a path for removing repositories
+    # We use `+` because its not allowed by GitLab so user can not create
+    # project with name cookies+119+deleted and capture someone stalled repository
+    #
+    # gitlab/cookies.git -> gitlab/cookies+119+deleted.git
+    #
+    def removal_path(path)
+      "#{path}+#{project.id}#{DELETED_FLAG}"
     end
   end
 end
diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb
index 530f9d93de4719479e1f6c6ebb835dbf584ef51e..172d4902add336e73894828004322d14a06969ab 100644
--- a/lib/gitlab/backend/shell.rb
+++ b/lib/gitlab/backend/shell.rb
@@ -244,6 +244,16 @@ module Gitlab
       end
     end
 
+    # Check if such directory exists in repositories.
+    #
+    # Usage:
+    #   exists?('gitlab')
+    #   exists?('gitlab/cookies.git')
+    #
+    def exists?(dir_name)
+      File.exists?(full_path(dir_name))
+    end
+
     protected
 
     def gitlab_shell_path
@@ -264,10 +274,6 @@ module Gitlab
       File.join(repos_path, dir_name)
     end
 
-    def exists?(dir_name)
-      File.exists?(full_path(dir_name))
-    end
-
     def gitlab_shell_projects_path
       File.join(gitlab_shell_path, 'bin', 'gitlab-projects')
     end