diff --git a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/migration/MigrationController.java b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/migration/MigrationController.java
index ca6c3b86bd2073aee7000fd77c7b833eaf10fb0e..1e423659f6b88228c64a8976bd18568f3509a3bd 100644
--- a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/migration/MigrationController.java
+++ b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/migration/MigrationController.java
@@ -30,7 +30,7 @@ public class MigrationController extends BaseController
 	{
 		public static final String ERROR = "error";
 		public static final String MIGRATION_SETTINGS = "migrationSettings";
-		public static final String STATUS_TEXT_KEY = "statusTextKey";
+		public static final String STATUS = "status";
 	}
 
 	private static class ReturnValues
@@ -90,24 +90,14 @@ public class MigrationController extends BaseController
 			return ReturnValues.MIGRATION_SETTINGS;
 		}
 
-		try
-		{
-			final MigrationArguments migrationArguments = new MigrationArguments.MigrationArgumentBuilder()
-					.withSourceUrl(migrationService.getDatabaseFromPreviousVersionPathWithoutExtension().toString())
-					.withDestinationUrl(migrationSettings.hostname(), migrationSettings.port(), migrationSettings.databaseName())
-					.withDestinationCredentials(migrationSettings.username(), migrationSettings.password())
-					.build();
-			// TODO: run non-blocking and redirect to progress page
-			migrationService.runMigration(migrationArguments);
-		}
-		catch(MigrationException | IOException e)
-		{
-			WebRequestUtils.putNotification(request, new Notification(Localization.getString("notification.migration.error", e.getMessage()), NotificationType.ERROR));
-			return ReturnValues.MIGRATION_SETTINGS;
-		}
+		final MigrationArguments migrationArguments = new MigrationArguments.MigrationArgumentBuilder()
+				.withSourceUrl(migrationService.getDatabaseFromPreviousVersionPathWithoutExtension().toString())
+				.withDestinationUrl(migrationSettings.hostname(), migrationSettings.port(), migrationSettings.databaseName())
+				.withDestinationCredentials(migrationSettings.username(), migrationSettings.password())
+				.build();
+		migrationService.startMigration(migrationArguments);
 
-		// TODO: redirect to success page
-		return ReturnValues.MIGRATION_SETTINGS;
+		return ReturnValues.STATUS;
 	}
 
 	@GetMapping("/status")
@@ -119,7 +109,7 @@ public class MigrationController extends BaseController
 	@GetMapping("/getStatus")
 	public String getMigrationStatus(Model model)
 	{
-		model.addAttribute(ModelAttributes.STATUS_TEXT_KEY, "migration.status.running");
+		model.addAttribute(ModelAttributes.STATUS, migrationService.getMigrationStatus());
 		return ReturnValues.STATUS_FRAGMENT;
 	}
 }
\ No newline at end of file
diff --git a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/migration/MigrationService.java b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/migration/MigrationService.java
index 69ef8050a30fbd3eac9df365143194266aeaa77e..c38fe2a6875adf1df654ac3aadf259c1f72591b8 100644
--- a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/migration/MigrationService.java
+++ b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/migration/MigrationService.java
@@ -8,31 +8,19 @@ import de.deadlocker8.budgetmaster.templates.TemplateRepository;
 import de.deadlocker8.budgetmaster.transactions.TransactionRepository;
 import de.deadlocker8.budgetmaster.utils.DatabaseConfigurationProperties;
 import de.deadlocker8.budgetmaster.utils.Mappings;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.TaskScheduler;
 import org.springframework.stereotype.Service;
 
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.nio.file.StandardCopyOption;
-import java.text.MessageFormat;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
+import java.util.Date;
 
 @Service
 public class MigrationService
 {
-	private static final Logger LOGGER = LoggerFactory.getLogger(MigrationService.class);
-
 	public static final String PREVIOUS_DATABASE_FILE_NAME = "budgetmaster.mv.db";
 	public static final String PREVIOUS_DATABASE_FILE_NAME_WITHOUT_EXTENSION = "budgetmaster";
-	private static final String BUDGET_MASTER_MIGRATOR_JAR = "BudgetMasterDatabaseMigrator.jar";
 	private final SettingsService settingsService;
 	private final Path applicationSupportFolder;
 	private final AccountRepository accountRepository;
@@ -40,9 +28,12 @@ public class MigrationService
 	private final TransactionRepository transactionRepository;
 	private final TemplateRepository templateRepository;
 	private final DatabaseConfigurationProperties databaseConfig;
+	private final TaskScheduler scheduler;
+
+	private final MigrationTask migrationTask;
 
 	@Autowired
-	public MigrationService(SettingsService settingsService, Path applicationSupportFolder, AccountRepository accountRepository, CategoryRepository categoryRepository, TransactionRepository transactionRepository, TemplateRepository templateRepository, DatabaseConfigurationProperties databaseConfig)
+	public MigrationService(SettingsService settingsService, Path applicationSupportFolder, AccountRepository accountRepository, CategoryRepository categoryRepository, TransactionRepository transactionRepository, TemplateRepository templateRepository, DatabaseConfigurationProperties databaseConfig, TaskScheduler scheduler)
 	{
 		this.settingsService = settingsService;
 		this.applicationSupportFolder = applicationSupportFolder;
@@ -51,6 +42,8 @@ public class MigrationService
 		this.transactionRepository = transactionRepository;
 		this.templateRepository = templateRepository;
 		this.databaseConfig = databaseConfig;
+		this.migrationTask = new MigrationTask(applicationSupportFolder);
+		this.scheduler = scheduler;
 	}
 
 	public MigrationSettings getPrefilledMigrationSettings()
@@ -114,85 +107,14 @@ public class MigrationService
 		return Files.exists(getDatabaseFromPreviousVersionPath());
 	}
 
-	public String runMigration(MigrationArguments migrationArguments) throws MigrationException, IOException
-	{
-		final Path migratorPath = extractMigrator();
-		try
-		{
-			return runMigrator(migratorPath, migrationArguments);
-		}
-		finally
-		{
-			Files.deleteIfExists(migratorPath);
-		}
-	}
-
-	private Path extractMigrator() throws MigrationException
-	{
-		final Path destinationPath = applicationSupportFolder.resolve(BUDGET_MASTER_MIGRATOR_JAR);
-
-		try
-		{
-			Files.copy(Objects.requireNonNull(getClass().getClassLoader().getResourceAsStream(BUDGET_MASTER_MIGRATOR_JAR)), destinationPath, StandardCopyOption.REPLACE_EXISTING);
-			return destinationPath;
-		}
-		catch(IOException e)
-		{
-			throw new MigrationException(MessageFormat.format("Could not copy migrator to {0}", destinationPath), e);
-		}
-	}
-
-	private String runMigrator(Path migratorPath, MigrationArguments migrationArguments) throws MigrationException
+	public void startMigration(MigrationArguments migrationArguments)
 	{
-		final String javaCommand = determineJavaCommand();
-
-		final List<String> command = new ArrayList<>();
-		command.add(MessageFormat.format("\"{0}\"", javaCommand));
-		command.add("-jar");
-		command.add(migratorPath.toString());
-		command.addAll(migrationArguments.getArguments());
-		LOGGER.debug("Starting migration with command: {}", command);
-
-		try
-		{
-			final ProcessBuilder processBuilder = new ProcessBuilder(command).redirectErrorStream(true);
-			final Process process = processBuilder.start();
-
-			final StringBuilder collectedStdout = new StringBuilder();
-			try(BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream())))
-			{
-				while(true)
-				{
-					String line = in.readLine();
-					if(line == null)
-					{
-						break;
-					}
-
-					LOGGER.debug("[MIGRATOR] {}", line);
-
-					collectedStdout.append(line);
-					collectedStdout.append("\n");
-				}
-			}
-
-			LOGGER.debug("Migration process finished");
-			return collectedStdout.toString();
-		}
-		catch(IOException e)
-		{
-			throw new MigrationException("Error during migration process", e);
-		}
+		migrationTask.setMigrationArguments(migrationArguments);
+		scheduler.schedule(migrationTask, new Date());
 	}
 
-	private String determineJavaCommand() throws MigrationException
+	public MigrationStatus getMigrationStatus()
 	{
-		final Optional<String> commandResultOptional = ProcessHandle.current().info().command();
-		if(commandResultOptional.isEmpty())
-		{
-			throw new MigrationException("Could not determine java executable");
-		}
-
-		return commandResultOptional.get().replace("\\", "/");
+		return migrationTask.getMigrationStatus();
 	}
 }
diff --git a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/migration/MigrationStatus.java b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/migration/MigrationStatus.java
new file mode 100644
index 0000000000000000000000000000000000000000..61e49a05bb461b2a1761ca899fc93074eba112a9
--- /dev/null
+++ b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/migration/MigrationStatus.java
@@ -0,0 +1,29 @@
+package de.deadlocker8.budgetmaster.migration;
+
+public enum MigrationStatus
+{
+	NOT_RUNNING("migration.status.not.running"),
+	RUNNING("migration.status.running"),
+	SUCCESS("migration.status.success"),
+	ERROR("migration.status.error");
+
+	private final String localizationKey;
+
+	MigrationStatus(String localizationKey)
+	{
+		this.localizationKey = localizationKey;
+	}
+
+	public String getLocalizationKey()
+	{
+		return localizationKey;
+	}
+
+	@Override
+	public String toString()
+	{
+		return "MigrationStatus{" +
+				"localizationKey='" + localizationKey + '\'' +
+				"} " + super.toString();
+	}
+}
diff --git a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/migration/MigrationTask.java b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/migration/MigrationTask.java
new file mode 100644
index 0000000000000000000000000000000000000000..c7a4de6a90ffc2645c9f830daf1f4e8f219f8e6a
--- /dev/null
+++ b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/migration/MigrationTask.java
@@ -0,0 +1,155 @@
+package de.deadlocker8.budgetmaster.migration;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+public class MigrationTask implements Runnable
+{
+	private static final Logger LOGGER = LoggerFactory.getLogger(MigrationTask.class);
+	private static final String BUDGET_MASTER_MIGRATOR_JAR = "BudgetMasterDatabaseMigrator.jar";
+
+	private final Path applicationSupportFolder;
+
+	private MigrationArguments migrationArguments;
+	private MigrationStatus migrationStatus;
+
+	public MigrationTask(Path applicationSupportFolder)
+	{
+		this.applicationSupportFolder = applicationSupportFolder;
+		this.migrationStatus = MigrationStatus.NOT_RUNNING;
+	}
+
+	public void setMigrationArguments(MigrationArguments migrationArguments)
+	{
+		this.migrationArguments = migrationArguments;
+	}
+
+	@Override
+	public void run()
+	{
+		if(migrationArguments == null)
+		{
+			throw new RuntimeException("No migration arguments set!");
+		}
+
+		if(migrationStatus != MigrationStatus.NOT_RUNNING)
+		{
+			throw new RuntimeException("Migration already performed!");
+		}
+
+		LOGGER.debug("Start migration...");
+		setMigrationStatus(MigrationStatus.RUNNING);
+
+		try
+		{
+			final Path migratorPath = extractMigrator();
+			try
+			{
+				runMigrator(migratorPath, migrationArguments);
+			}
+			finally
+			{
+				Files.deleteIfExists(migratorPath);
+			}
+
+			setMigrationStatus(MigrationStatus.SUCCESS);
+		}
+		catch(MigrationException | IOException e)
+		{
+			LOGGER.error("Migration failed", e);
+			setMigrationStatus(MigrationStatus.ERROR);
+		}
+	}
+
+	private Path extractMigrator() throws MigrationException
+	{
+		final Path destinationPath = applicationSupportFolder.resolve(BUDGET_MASTER_MIGRATOR_JAR);
+
+		try
+		{
+			Files.copy(Objects.requireNonNull(getClass().getClassLoader().getResourceAsStream(BUDGET_MASTER_MIGRATOR_JAR)), destinationPath, StandardCopyOption.REPLACE_EXISTING);
+			return destinationPath;
+		}
+		catch(IOException e)
+		{
+			throw new MigrationException(MessageFormat.format("Could not copy migrator to {0}", destinationPath), e);
+		}
+	}
+
+	private String runMigrator(Path migratorPath, MigrationArguments migrationArguments) throws MigrationException
+	{
+		final String javaCommand = determineJavaCommand();
+
+		final List<String> command = new ArrayList<>();
+		command.add(MessageFormat.format("\"{0}\"", javaCommand));
+		command.add("-jar");
+		command.add(migratorPath.toString());
+		command.addAll(migrationArguments.getArguments());
+		LOGGER.debug("Starting migration with command: {}", command);
+
+		try
+		{
+			final ProcessBuilder processBuilder = new ProcessBuilder(command).redirectErrorStream(true);
+			final Process process = processBuilder.start();
+
+			final StringBuilder collectedStdout = new StringBuilder();
+			try(BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream())))
+			{
+				while(true)
+				{
+					String line = in.readLine();
+					if(line == null)
+					{
+						break;
+					}
+
+					LOGGER.debug("[MIGRATOR] {}", line);
+
+					collectedStdout.append(line);
+					collectedStdout.append("\n");
+				}
+			}
+
+			LOGGER.debug("Migration process finished");
+			return collectedStdout.toString();
+		}
+		catch(IOException e)
+		{
+			throw new MigrationException("Error during migration process", e);
+		}
+	}
+
+	private String determineJavaCommand() throws MigrationException
+	{
+		final Optional<String> commandResultOptional = ProcessHandle.current().info().command();
+		if(commandResultOptional.isEmpty())
+		{
+			throw new MigrationException("Could not determine java executable");
+		}
+
+		return commandResultOptional.get().replace("\\", "/");
+	}
+
+
+	public MigrationStatus getMigrationStatus()
+	{
+		return migrationStatus;
+	}
+
+	public void setMigrationStatus(MigrationStatus migrationStatus)
+	{
+		this.migrationStatus = migrationStatus;
+	}
+}
diff --git a/BudgetMasterServer/src/main/resources/languages/base_de.properties b/BudgetMasterServer/src/main/resources/languages/base_de.properties
index deac411684821ee2151fb2181f7a1fd989de4448..0fddc3c8902cf68367d87c8f282ded97ab0176f9 100644
--- a/BudgetMasterServer/src/main/resources/languages/base_de.properties
+++ b/BudgetMasterServer/src/main/resources/languages/base_de.properties
@@ -649,6 +649,7 @@ migration.settings.password=Passwort
 migration.settings.verification.password.description=Bitte gib dein aktuelles BudgetMaster Passwort ein, um den Migrationsprozess zu bestätigen.<br><span class="red-text bold">Die Migration kann nicht abgebrochen werden.</span><br>Deine bestehende BudgetMaster Datenbank wird weder geändert noch gelöscht.
 migration.settings.verification.password=Aktuelles BudgetMaster Passwort
 migration.settings.verification.password.wrong=Ungültiges Bestätigungspasswort
+migration.status.not.running=Migration wurde nicht gestartet.
 migration.status.running=Migration läuft...
-migration.status.finished=Migration fertiggestellt!
+migration.status.success=Migration fertiggestellt!
 migration.status.error=Migration fehlgeschlagen!
diff --git a/BudgetMasterServer/src/main/resources/languages/base_en.properties b/BudgetMasterServer/src/main/resources/languages/base_en.properties
index 8166a4ba44d9156d1c4fbc778160a1c99e1c19bb..b672a014375f5075359aab3c3cd28b15f107b23a 100644
--- a/BudgetMasterServer/src/main/resources/languages/base_en.properties
+++ b/BudgetMasterServer/src/main/resources/languages/base_en.properties
@@ -648,7 +648,8 @@ migration.settings.password=Password
 migration.settings.verification.password.description=Please enter your current BudgetMaster password to confirm the migration process.<br><span class="red-text bold">The migration can not be canceled.</span><br>Your existing BudgetMaster database will neither be modified nor deleted.
 migration.settings.verification.password=Current BudgetMaster password
 migration.settings.verification.password.wrong=Invalid verification password
+migration.status.not.running=Migration is not started.
 migration.status.running=Migration is running...
-migration.status.finished=Migration finished!
+migration.status.success=Migration finished!
 migration.status.error=Migration failed!
 
diff --git a/BudgetMasterServer/src/main/resources/templates/migration/statusFragment.ftl b/BudgetMasterServer/src/main/resources/templates/migration/statusFragment.ftl
index 80073e5b80ded45dff24a0ef1408d4285e9f0e04..83e8da527437683589b1774e63873486eb16341d 100644
--- a/BudgetMasterServer/src/main/resources/templates/migration/statusFragment.ftl
+++ b/BudgetMasterServer/src/main/resources/templates/migration/statusFragment.ftl
@@ -4,12 +4,11 @@
 
 <div class="row">
     <div class="col s12 m12 l8 offset-l2">
-        <div class="headline-small">${locale.getString(statusTextKey)}</div>
+        <div class="headline-small">${locale.getString(status.getLocalizationKey())}</div>
     </div>
 </div>
 
 <div class="row">
     <div class="col s12 m12 l8 offset-l2">
-        Status:
     </div>
 </div>
\ No newline at end of file
diff --git a/BudgetMasterServer/src/test/java/de/deadlocker8/budgetmaster/unit/MigrationServiceTest.java b/BudgetMasterServer/src/test/java/de/deadlocker8/budgetmaster/unit/MigrationServiceTest.java
index e6aa9c4ef30d7f6c37a4995368322fee97a94ad3..430c4b89d186ff3ae18274e3988064710d7c0152 100644
--- a/BudgetMasterServer/src/test/java/de/deadlocker8/budgetmaster/unit/MigrationServiceTest.java
+++ b/BudgetMasterServer/src/test/java/de/deadlocker8/budgetmaster/unit/MigrationServiceTest.java
@@ -17,6 +17,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
 import org.junit.jupiter.api.io.TempDir;
 import org.mockito.Mock;
 import org.mockito.Mockito;
+import org.springframework.scheduling.TaskScheduler;
 import org.springframework.test.context.junit.jupiter.SpringExtension;
 
 import java.io.IOException;
@@ -48,6 +49,9 @@ class MigrationServiceTest
 	@Mock
 	private DatabaseConfigurationProperties databaseConfig;
 
+	@Mock
+	private TaskScheduler scheduler;
+
 	@TempDir
 	public Path tempFolder;
 
@@ -61,7 +65,7 @@ class MigrationServiceTest
 
 		Mockito.when(settingsService.getSettings()).thenReturn(settings);
 
-		final MigrationService migrationService = new MigrationService(settingsService, tempFolder, accountRepository, categoryRepository, transactionRepository, templateRepository, databaseConfig);
+		final MigrationService migrationService = new MigrationService(settingsService, tempFolder, accountRepository, categoryRepository, transactionRepository, templateRepository, databaseConfig, scheduler);
 		assertThat(migrationService.needToShowMigrationDialog("migration")).isFalse();
 	}
 
@@ -76,7 +80,7 @@ class MigrationServiceTest
 
 		Mockito.when(settingsService.getSettings()).thenReturn(settings);
 
-		final MigrationService migrationService = new MigrationService(settingsService, tempFolder, accountRepository, categoryRepository, transactionRepository, templateRepository, databaseConfig);
+		final MigrationService migrationService = new MigrationService(settingsService, tempFolder, accountRepository, categoryRepository, transactionRepository, templateRepository, databaseConfig, scheduler);
 		assertThat(migrationService.needToShowMigrationDialog(Mappings.TRANSACTIONS)).isFalse();
 	}
 
@@ -91,7 +95,7 @@ class MigrationServiceTest
 		Mockito.when(settingsService.getSettings()).thenReturn(settings);
 		Mockito.when(categoryRepository.findAllByTypeOrderByNameAsc(CategoryType.CUSTOM)).thenReturn(List.of(new Category("custom category", "ff0000", CategoryType.CUSTOM)));
 
-		final MigrationService migrationService = new MigrationService(settingsService, tempFolder, accountRepository, categoryRepository, transactionRepository, templateRepository, databaseConfig);
+		final MigrationService migrationService = new MigrationService(settingsService, tempFolder, accountRepository, categoryRepository, transactionRepository, templateRepository, databaseConfig, scheduler);
 		assertThat(migrationService.needToShowMigrationDialog(Mappings.TRANSACTIONS)).isFalse();
 	}
 
@@ -102,7 +106,7 @@ class MigrationServiceTest
 
 		Mockito.when(settingsService.getSettings()).thenReturn(settings);
 
-		final MigrationService migrationService = new MigrationService(settingsService, tempFolder, accountRepository, categoryRepository, transactionRepository, templateRepository, databaseConfig);
+		final MigrationService migrationService = new MigrationService(settingsService, tempFolder, accountRepository, categoryRepository, transactionRepository, templateRepository, databaseConfig, scheduler);
 		assertThat(migrationService.needToShowMigrationDialog(Mappings.TRANSACTIONS)).isFalse();
 	}
 
@@ -116,7 +120,7 @@ class MigrationServiceTest
 
 		Mockito.when(settingsService.getSettings()).thenReturn(settings);
 
-		final MigrationService migrationService = new MigrationService(settingsService, tempFolder, accountRepository, categoryRepository, transactionRepository, templateRepository, databaseConfig);
+		final MigrationService migrationService = new MigrationService(settingsService, tempFolder, accountRepository, categoryRepository, transactionRepository, templateRepository, databaseConfig, scheduler);
 		assertThat(migrationService.needToShowMigrationDialog(Mappings.TRANSACTIONS)).isTrue();
 	}
 }