diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxService.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxService.kt
index c13b1a5a0307fc9b256d17fe74c3ed37a5d7b2ed..ed09f281995879a5db6a09b1684b4fe13254c9a9 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxService.kt
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxService.kt
@@ -29,13 +29,13 @@ import androidx.core.content.ContextCompat
 import dagger.hilt.android.AndroidEntryPoint
 import org.briarproject.mailbox.android.MailboxNotificationManager.Companion.NOTIFICATION_MAIN_ID
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager
-import org.briarproject.mailbox.core.lifecycle.LifecycleManager.StartResult
-import org.briarproject.mailbox.core.lifecycle.LifecycleManager.StartResult.ALREADY_RUNNING
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager.StartResult.SUCCESS
+import org.briarproject.mailbox.core.system.AndroidWakeLock
 import org.briarproject.mailbox.core.system.AndroidWakeLockManager
 import org.slf4j.LoggerFactory.getLogger
 import java.util.concurrent.atomic.AtomicBoolean
 import javax.inject.Inject
+import kotlin.concurrent.thread
 import kotlin.system.exitProcess
 
 @AndroidEntryPoint
@@ -70,52 +70,60 @@ class MailboxService : Service() {
     @Inject
     internal lateinit var notificationManager: MailboxNotificationManager
 
+    private lateinit var lifecycleWakeLock: AndroidWakeLock
+
     override fun onCreate() {
         super.onCreate()
 
         LOG.info("Created")
         if (created.getAndSet(true)) {
             LOG.warn("Already created")
-            // FIXME when can this happen? Next line will kill app
+            // This is a canary to notify us about strange behavior concerning service creation
+            // in logs and bug reports. Calling stopSelf() kills the app.
             stopSelf()
             return
         }
 
-        // Hold a wake lock during startup
-        wakeLockManager.runWakefully({
-            startForeground(NOTIFICATION_MAIN_ID, notificationManager.serviceNotification)
-            // Start the services in a background thread
-            wakeLockManager.executeWakefully({
-                val result: StartResult = lifecycleManager.startServices()
-                when {
-                    result === SUCCESS -> started = true
-                    result === ALREADY_RUNNING -> {
-                        LOG.warn("Already running")
-                        // FIXME when can this happen? Next line will kill app
-                        stopSelf()
-                    }
-                    else -> {
-                        if (LOG.isWarnEnabled) LOG.warn("Startup failed: $result")
-                        // TODO: implement this
-                        //  and start activity in new process, so we can kill this one
-                        // showStartupFailure(result)
-                        stopSelf()
-                    }
-                }
-            }, "LifecycleStartup")
-            // Register for device shutdown broadcasts
-            receiver = object : BroadcastReceiver() {
-                override fun onReceive(context: Context, intent: Intent) {
-                    LOG.info("Device is shutting down")
+        startForeground(NOTIFICATION_MAIN_ID, notificationManager.serviceNotification)
+
+        // We hold a wake lock during the whole lifecycle. We have a one-to-one relationship
+        // between MailboxService and the LifecycleManager. As we do not support lifecycle restarts
+        // only a single MailboxService is allowed to start and stop with the LifecycleManager
+        // singleton. Should the service be killed and restarted, the LifecycleManager must also
+        // have been destroyed and there is no way to recover except via a restart of the app.
+        // So should a second MailboxService be started anytime, this is a unrecoverable situation
+        // and we stop the app.
+        // Acquiring the wakelock here and releasing it as the last thing before exitProcess()
+        // during onDestroy() makes sure it is being held during the whole lifecycle.
+        lifecycleWakeLock = wakeLockManager.createWakeLock("Lifecycle")
+        lifecycleWakeLock.acquire()
+
+        // Start the services in a background thread
+        thread {
+            val result = lifecycleManager.startServices()
+            when {
+                result === SUCCESS -> started = true
+                else -> {
+                    if (LOG.isWarnEnabled) LOG.warn("Startup failed: $result")
+                    // TODO: implement this
+                    //  and start activity in new process, so we can kill this one
+                    // showStartupFailure(result)
                     stopSelf()
                 }
             }
-            val filter = IntentFilter()
-            filter.addAction(Intent.ACTION_SHUTDOWN)
-            filter.addAction("android.intent.action.QUICKBOOT_POWEROFF")
-            filter.addAction("com.htc.intent.action.QUICKBOOT_POWEROFF")
-            registerReceiver(receiver, filter)
-        }, "LifecycleStartup")
+        }
+        // Register for device shutdown broadcasts
+        receiver = object : BroadcastReceiver() {
+            override fun onReceive(context: Context, intent: Intent) {
+                LOG.info("Device is shutting down")
+                stopSelf()
+            }
+        }
+        val filter = IntentFilter()
+        filter.addAction(Intent.ACTION_SHUTDOWN)
+        filter.addAction("android.intent.action.QUICKBOOT_POWEROFF")
+        filter.addAction("com.htc.intent.action.QUICKBOOT_POWEROFF")
+        registerReceiver(receiver, filter)
     }
 
     override fun onBind(intent: Intent): IBinder? {
@@ -123,23 +131,23 @@ class MailboxService : Service() {
     }
 
     override fun onDestroy() {
-        wakeLockManager.runWakefully({
-            super.onDestroy()
-            LOG.info("Destroyed")
-            stopForeground(true)
-            if (receiver != null) unregisterReceiver(receiver)
-            wakeLockManager.executeWakefully({
-                try {
-                    if (started) {
-                        lifecycleManager.stopServices()
-                        lifecycleManager.waitForShutdown()
-                    }
-                } catch (e: InterruptedException) {
-                    LOG.info("Interrupted while waiting for shutdown")
-                }
-                LOG.info("Exiting")
-                exitProcess(0)
-            }, "LifecycleShutdown")
-        }, "LifecycleShutdown")
+        super.onDestroy()
+        LOG.info("Destroyed")
+        stopForeground(true)
+        if (receiver != null) unregisterReceiver(receiver)
+        if (started) {
+            try {
+                lifecycleManager.stopServices()
+                lifecycleManager.waitForShutdown()
+            } catch (e: InterruptedException) {
+                LOG.info("Interrupted while waiting for shutdown")
+            }
+        }
+        // Do not exit within wakeful execution, otherwise we will never release the wake locks.
+        // Or maybe we want to do precisely that to make sure exiting really happens and the app
+        // doesn't get suspended before it gets a chance to exit?
+        lifecycleWakeLock.release()
+        LOG.info("Exiting")
+        exitProcess(0)
     }
 }
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxViewModel.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxViewModel.kt
index 6730309c147abdb859c35a6ebd52057170b17d56..7cdf4dfa16a8b100f4af24653a055ceaae941278 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxViewModel.kt
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxViewModel.kt
@@ -30,9 +30,9 @@ import kotlinx.coroutines.flow.StateFlow
 import org.briarproject.android.dontkillmelib.DozeHelper
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState
-import org.briarproject.mailbox.core.system.AndroidWakeLockManager
 import org.briarproject.mailbox.core.system.DozeWatchdog
 import javax.inject.Inject
+import kotlin.concurrent.thread
 
 @HiltViewModel
 class MailboxViewModel @Inject constructor(
@@ -41,7 +41,6 @@ class MailboxViewModel @Inject constructor(
     private val dozeWatchdog: DozeWatchdog,
     handle: SavedStateHandle,
     private val lifecycleManager: LifecycleManager,
-    private val wakeLockManager: AndroidWakeLockManager,
 ) : AndroidViewModel(app) {
 
     val needToShowDoNotKillMeFragment get() = dozeHelper.needToShowDoNotKillMeFragment(app)
@@ -68,10 +67,11 @@ class MailboxViewModel @Inject constructor(
     }
 
     fun wipe() {
-        wakeLockManager.executeWakefully({
+        thread {
+            // TODO: handle return value
             lifecycleManager.wipeMailbox()
             MailboxService.stopService(getApplication())
-        }, "LifecycleWipe")
+        }
     }
 
     fun getAndResetDozeFlag() = dozeWatchdog.andResetDozeFlag
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/core/system/AndroidTaskScheduler.java b/mailbox-android/src/main/java/org/briarproject/mailbox/core/system/AndroidTaskScheduler.java
index 56df13486773ef9d0b0916a54723b972b1f51e20..7788735c0f21f59d6a29343128c83a996e0cf644 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/core/system/AndroidTaskScheduler.java
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/core/system/AndroidTaskScheduler.java
@@ -63,7 +63,6 @@ public class AndroidTaskScheduler implements TaskScheduler, Service {
 	private static final long ALARM_MS = INTERVAL_FIFTEEN_MINUTES;
 
 	private final Application app;
-	private final AndroidWakeLockManager wakeLockManager;
 	private final ScheduledExecutorService scheduledExecutorService;
 	private final AlarmManager alarmManager;
 
@@ -72,10 +71,8 @@ public class AndroidTaskScheduler implements TaskScheduler, Service {
 	private final Queue<ScheduledTask> tasks = new PriorityQueue<>();
 
 	AndroidTaskScheduler(Application app,
-			AndroidWakeLockManager wakeLockManager,
 			ScheduledExecutorService scheduledExecutorService) {
 		this.app = app;
-		this.wakeLockManager = wakeLockManager;
 		this.scheduledExecutorService = scheduledExecutorService;
 		alarmManager = (AlarmManager) requireNonNull(
 				app.getSystemService(ALARM_SERVICE));
@@ -107,29 +104,25 @@ public class AndroidTaskScheduler implements TaskScheduler, Service {
 	}
 
 	public void onAlarm(Intent intent) {
-		wakeLockManager.runWakefully(() -> {
-			int extraPid = intent.getIntExtra(EXTRA_PID, -1);
-			int currentPid = Process.myPid();
-			if (extraPid == currentPid) {
-				LOG.info("Alarm");
-				rescheduleAlarm();
-				runDueTasks();
-			} else {
-				info(LOG, () -> "Ignoring alarm with PID " + extraPid +
-						", current PID is " +
-						currentPid);
-			}
-		}, "TaskAlarm");
+		int extraPid = intent.getIntExtra(EXTRA_PID, -1);
+		int currentPid = Process.myPid();
+		if (extraPid == currentPid) {
+			LOG.info("Alarm");
+			rescheduleAlarm();
+			runDueTasks();
+		} else {
+			info(LOG, () -> "Ignoring alarm with PID " + extraPid +
+					", current PID is " + currentPid);
+		}
 	}
 
 	private Cancellable schedule(Runnable task, Executor executor, long delay,
 			TimeUnit unit, AtomicBoolean cancelled) {
 		long now = SystemClock.elapsedRealtime();
 		long dueMillis = now + MILLISECONDS.convert(delay, unit);
-		Runnable wakeful = () ->
-				wakeLockManager.executeWakefully(task, executor, "TaskHandoff");
+		Runnable wrapped = () -> executor.execute(task);
 		Future<?> check = scheduleCheckForDueTasks(delay, unit);
-		ScheduledTask s = new ScheduledTask(wakeful, dueMillis, check,
+		ScheduledTask s = new ScheduledTask(wrapped, dueMillis, check,
 				cancelled);
 		synchronized (lock) {
 			tasks.add(s);
@@ -149,9 +142,8 @@ public class AndroidTaskScheduler implements TaskScheduler, Service {
 	}
 
 	private Future<?> scheduleCheckForDueTasks(long delay, TimeUnit unit) {
-		Runnable wakeful = () -> wakeLockManager.runWakefully(
-				this::runDueTasks, "TaskScheduler");
-		return scheduledExecutorService.schedule(wakeful, delay, unit);
+		return scheduledExecutorService
+				.schedule(this::runDueTasks, delay, unit);
 	}
 
 	@Wakeful
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/core/system/AndroidTaskSchedulerModule.java b/mailbox-android/src/main/java/org/briarproject/mailbox/core/system/AndroidTaskSchedulerModule.java
index 72733855e7bc5bef9cc55de6bf4d304826469179..0c5383f8fcb2c2e752207e1fe6e50d6fa38db03b 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/core/system/AndroidTaskSchedulerModule.java
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/core/system/AndroidTaskSchedulerModule.java
@@ -43,7 +43,7 @@ public class AndroidTaskSchedulerModule {
 			AndroidWakeLockManager wakeLockManager,
 			ScheduledExecutorService scheduledExecutorService) {
 		AndroidTaskScheduler scheduler = new AndroidTaskScheduler(app,
-				wakeLockManager, scheduledExecutorService);
+				scheduledExecutorService);
 		lifecycleManager.registerService(scheduler);
 		return scheduler;
 	}
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/core/system/AndroidWakeLockManager.java b/mailbox-android/src/main/java/org/briarproject/mailbox/core/system/AndroidWakeLockManager.java
index 449828b396ee77a736ba5c2950d0ce5d153c22c4..8d717149a29c837393a378cf1ac39e82544c86b2 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/core/system/AndroidWakeLockManager.java
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/core/system/AndroidWakeLockManager.java
@@ -19,8 +19,6 @@
 
 package org.briarproject.mailbox.core.system;
 
-import java.util.concurrent.Executor;
-
 public interface AndroidWakeLockManager {
 
 	/**
@@ -28,27 +26,4 @@ public interface AndroidWakeLockManager {
 	 * logging; the underlying OS wake lock will use its own tag.
 	 */
 	AndroidWakeLock createWakeLock(String tag);
-
-	/**
-	 * Runs the given task while holding a wake lock.
-	 */
-	void runWakefully(Runnable r, String tag);
-
-	/**
-	 * Submits the given task to the given executor while holding a wake lock.
-	 * The lock is released when the task completes, or if an exception is
-	 * thrown while submitting or running the task.
-	 */
-	void executeWakefully(Runnable r, Executor executor, String tag);
-
-	/**
-	 * Starts a dedicated thread to run the given task asynchronously. A wake
-	 * lock is acquired before starting the thread and released when the task
-	 * completes, or if an exception is thrown while starting the thread or
-	 * running the task.
-	 * <p>
-	 * This method should only be used for lifecycle management tasks that
-	 * can't be run on an executor.
-	 */
-	void executeWakefully(Runnable r, String tag);
 }
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/core/system/AndroidWakeLockManagerImpl.java b/mailbox-android/src/main/java/org/briarproject/mailbox/core/system/AndroidWakeLockManagerImpl.java
index 13bbfbe4a44c7694a78b60247bbd8d3ae1b354f4..c00275eff38f81abe57be4cbf39db37d2073a64a 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/core/system/AndroidWakeLockManagerImpl.java
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/core/system/AndroidWakeLockManagerImpl.java
@@ -24,7 +24,6 @@ import android.content.Context;
 import android.content.pm.PackageManager;
 import android.os.PowerManager;
 
-import java.util.concurrent.Executor;
 import java.util.concurrent.ScheduledExecutorService;
 
 import javax.inject.Inject;
@@ -66,57 +65,6 @@ class AndroidWakeLockManagerImpl implements AndroidWakeLockManager {
 		return new AndroidWakeLockImpl(sharedWakeLock, tag);
 	}
 
-	@Override
-	public void runWakefully(Runnable r, String tag) {
-		AndroidWakeLock wakeLock = createWakeLock(tag);
-		wakeLock.acquire();
-		try {
-			r.run();
-		} finally {
-			wakeLock.release();
-		}
-	}
-
-	@Override
-	public void executeWakefully(Runnable r, Executor executor, String tag) {
-		AndroidWakeLock wakeLock = createWakeLock(tag);
-		wakeLock.acquire();
-		try {
-			executor.execute(() -> {
-				try {
-					r.run();
-				} finally {
-					// Release the wake lock if the task throws an exception
-					wakeLock.release();
-				}
-			});
-		} catch (Exception e) {
-			// Release the wake lock if the executor throws an exception when
-			// we submit the task (in which case the release() call above won't
-			// happen)
-			wakeLock.release();
-			throw e;
-		}
-	}
-
-	@Override
-	public void executeWakefully(Runnable r, String tag) {
-		AndroidWakeLock wakeLock = createWakeLock(tag);
-		wakeLock.acquire();
-		try {
-			new Thread(() -> {
-				try {
-					r.run();
-				} finally {
-					wakeLock.release();
-				}
-			}).start();
-		} catch (Exception e) {
-			wakeLock.release();
-			throw e;
-		}
-	}
-
 	private String getWakeLockTag(Context ctx) {
 		PackageManager pm = ctx.getPackageManager();
 		if (isInstalled(pm, "com.huawei.powergenie")) {
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/JdbcDatabase.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/JdbcDatabase.kt
index 50db543863776013d2ebf05d56ba22ea7753ce08..208d5f2f8e3289bf49a8c0fe4530125957e8b669 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/JdbcDatabase.kt
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/JdbcDatabase.kt
@@ -124,7 +124,7 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc
             listener?.onDatabaseCompaction()
             val start: Long = now()
             compactAndClose()
-            logDuration(LOG, { "Compacting database" }, start)
+            logDuration(LOG, start) { "Compacting database" }
             // Allow the next transaction to reopen the DB
             connectionsLock.lock()
             try {
@@ -210,10 +210,10 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc
         val start = now()
         if (readOnly) {
             lock.readLock().lock()
-            logDuration(LOG, { "Waiting for read lock" }, start)
+            logDuration(LOG, start) { "Waiting for read lock" }
         } else {
             lock.writeLock().lock()
-            logDuration(LOG, { "Waiting for write lock" }, start)
+            logDuration(LOG, start) { "Waiting for write lock" }
         }
         return try {
             Transaction(startTransaction(), readOnly)
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManager.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManager.java
index af614491e37b9624e60829d67e776e1f50e33e93..4250ab95722cfd1c107407e25824fd2f5172e83a 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManager.java
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManager.java
@@ -38,7 +38,6 @@ public interface LifecycleManager {
 	 * The result of calling {@link #startServices()}.
 	 */
 	enum StartResult {
-		ALREADY_RUNNING,
 		SERVICE_ERROR,
 		SUCCESS
 	}
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManagerImpl.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManagerImpl.kt
index 2e4781ed23032f3e6a7fcc1deec6717d0f5096f7..e3c5e18ae7d4e8f100cf2b285b5f71468cd790c9 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManagerImpl.kt
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManagerImpl.kt
@@ -35,7 +35,6 @@ import org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState.S
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState.WIPING
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager.OpenDatabaseHook
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager.StartResult
-import org.briarproject.mailbox.core.lifecycle.LifecycleManager.StartResult.ALREADY_RUNNING
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager.StartResult.SERVICE_ERROR
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager.StartResult.SUCCESS
 import org.briarproject.mailbox.core.setup.WipeManager
@@ -85,33 +84,37 @@ internal class LifecycleManagerImpl @Inject constructor(
     }
 
     override fun registerService(s: Service) {
-        LOG.info { "Registering service ${s.javaClass.simpleName}" }
+        LOG.info { "Registering service ${s.name()}" }
         services.add(s)
     }
 
     override fun registerOpenDatabaseHook(hook: OpenDatabaseHook) {
-        LOG.info { "Registering open database hook ${hook.javaClass.simpleName}" }
+        LOG.info { "Registering open database hook ${hook.name()}" }
         openDatabaseHooks.add(hook)
     }
 
     override fun registerForShutdown(e: ExecutorService) {
-        LOG.info { "Registering executor ${e.javaClass.simpleName}" }
+        LOG.info { "Registering executor ${e.name()}" }
         executors.add(e)
     }
 
     @GuardedBy("startStopWipeSemaphore")
     override fun startServices(): StartResult {
-        if (!startStopWipeSemaphore.tryAcquire()) {
-            LOG.info("Already starting or stopping")
-            return ALREADY_RUNNING
+        try {
+            startStopWipeSemaphore.acquire()
+        } catch (e: InterruptedException) {
+            LOG.warn("Interrupted while waiting to start services")
+            return SERVICE_ERROR
+        }
+        if (!state.compareAndSet(NOT_STARTED, STARTING)) {
+            return SERVICE_ERROR
         }
-        state.compareAndSet(NOT_STARTED, STARTING)
         return try {
             LOG.info("Opening database")
             var start = now()
             val reopened = db.open(this)
-            if (reopened) logDuration(LOG, { "Reopening database" }, start)
-            else logDuration(LOG, { "Creating database" }, start)
+            if (reopened) logDuration(LOG, start) { "Reopening database" }
+            else logDuration(LOG, start) { "Creating database" }
             // Inform hooks that DB was opened
             db.write { txn ->
                 for (hook in openDatabaseHooks) {
@@ -124,7 +127,7 @@ internal class LifecycleManagerImpl @Inject constructor(
             for (s in services) {
                 start = now()
                 s.startService()
-                logDuration(LOG, { "Starting service  ${s.javaClass.simpleName}" }, start)
+                logDuration(LOG, start) { "Starting service  ${s.name()}" }
             }
             state.compareAndSet(STARTING_SERVICES, RUNNING)
             startupLatch.countDown()
@@ -166,15 +169,10 @@ internal class LifecycleManagerImpl @Inject constructor(
             val wiped = state.value == WIPING
             LOG.info("Stopping services")
             state.value = STOPPING
-            for (s in services) {
-                val start = now()
-                s.stopService()
-                logDuration(LOG, { "Stopping service " + s.javaClass.simpleName }, start)
-            }
-            for (e in executors) {
-                LOG.trace { "Stopping executor ${e.javaClass.simpleName}" }
-                e.shutdownNow()
-            }
+
+            stopAllServices()
+            stopAllExecutors()
+
             if (wiped) {
                 // If we just wiped, the database has already been closed, so we should not call
                 // close(). Since the services are being shut down after wiping (so that the web
@@ -182,21 +180,40 @@ internal class LifecycleManagerImpl @Inject constructor(
                 // API call created some files in the meantime. To make sure we delete those in
                 // case of a wipe, repeat deletion of files here after the services have been
                 // stopped.
-                wipeManager.wipe(wipeDatabase = false)
+                run("wiping files again") {
+                    wipeManager.wipeFilesOnly()
+                }
             } else {
-                val start = now()
-                db.close()
-                logDuration(LOG, { "Closing database" }, start)
+                run("closing database") {
+                    db.close()
+                }
             }
+
             shutdownLatch.countDown()
-        } catch (e: ServiceException) {
-            logException(LOG, e)
         } finally {
             state.compareAndSet(STOPPING, STOPPED)
             startStopWipeSemaphore.release()
         }
     }
 
+    @GuardedBy("startStopWipeSemaphore")
+    private fun stopAllServices() {
+        for (s in services) {
+            run("stopping service ${s.name()}") {
+                s.stopService()
+            }
+        }
+    }
+
+    @GuardedBy("startStopWipeSemaphore")
+    private fun stopAllExecutors() {
+        for (e in executors) {
+            run("stopping executor ${e.name()}") {
+                e.shutdownNow()
+            }
+        }
+    }
+
     @GuardedBy("startStopWipeSemaphore")
     override fun wipeMailbox(): Boolean {
         try {
@@ -209,8 +226,9 @@ internal class LifecycleManagerImpl @Inject constructor(
             return false
         }
         try {
-            wipeManager.wipe(wipeDatabase = true)
-
+            run("wiping database and files") {
+                wipeManager.wipeDatabaseAndFiles()
+            }
             // We need to move this to a thread so that the webserver call can finish when it calls
             // this. Otherwise we'll end up in a deadlock: the same thread trying to stop the
             // webserver from within a call that wants to send a response on the very same webserver.
@@ -219,7 +237,6 @@ internal class LifecycleManagerImpl @Inject constructor(
             thread {
                 stopServices()
             }
-
             return true
         } finally {
             startStopWipeSemaphore.release()
@@ -227,25 +244,28 @@ internal class LifecycleManagerImpl @Inject constructor(
     }
 
     @Throws(InterruptedException::class)
-    override fun waitForDatabase() {
-        dbLatch.await()
-    }
+    override fun waitForDatabase() = dbLatch.await()
 
     @Throws(InterruptedException::class)
-    override fun waitForStartup() {
-        startupLatch.await()
-    }
+    override fun waitForStartup() = startupLatch.await()
 
     @Throws(InterruptedException::class)
-    override fun waitForShutdown() {
-        shutdownLatch.await()
-    }
+    override fun waitForShutdown() = shutdownLatch.await()
 
-    override fun getLifecycleState(): LifecycleState {
-        return state.value
-    }
+    override fun getLifecycleState() = state.value
 
-    override fun getLifecycleStateFlow(): StateFlow<LifecycleState> {
-        return state
+    override fun getLifecycleStateFlow(): StateFlow<LifecycleState> = state
+
+    private fun run(name: String, task: () -> Unit) {
+        LOG.trace { name }
+        val start = now()
+        try {
+            task()
+        } catch (throwable: Throwable) {
+            logException(LOG, throwable) { "Error while $name" }
+        }
+        logDuration(LOG, start) { name }
     }
+
+    private fun Any.name() = javaClass.simpleName
 }
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/WipeManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/WipeManager.kt
index 68250a4a1fd9b947503a25c4097b42f7724edabd..59629b3cd8459a2a03b562559ce2f84f13dba434 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/WipeManager.kt
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/WipeManager.kt
@@ -42,12 +42,17 @@ class WipeManager @Inject constructor(
     /*
      * This must only be called by the LifecycleManager
      */
-    fun wipe(wipeDatabase: Boolean) {
-        if (wipeDatabase) {
-            db.dropAllTablesAndClose()
-            val dir = databaseConfig.getDatabaseDirectory()
-            IoUtils.deleteFileOrDir(dir)
-        }
+    fun wipeDatabaseAndFiles() {
+        db.dropAllTablesAndClose()
+        val dir = databaseConfig.getDatabaseDirectory()
+        IoUtils.deleteFileOrDir(dir)
+        fileManager.deleteAllFiles()
+    }
+
+    /*
+     * This must only be called by the LifecycleManager
+     */
+    fun wipeFilesOnly() {
         fileManager.deleteAllFiles()
     }
 
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/LogUtils.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/LogUtils.kt
index 7adf285c8aa598a7a6746914218c7d9b9f9c97f5..a0ae2f033abc2a1a5e54c76ad9bc72e241ffacf6 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/LogUtils.kt
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/LogUtils.kt
@@ -68,13 +68,18 @@ object LogUtils {
      * @param start  the start time of the task, as returned by [now]
      */
     @JvmStatic
-    fun logDuration(logger: Logger, msg: () -> String, start: Long) {
+    fun logDuration(logger: Logger, start: Long, msg: () -> String) {
         logger.trace {
             val duration = now() - start
             "${msg()} took $duration ms"
         }
     }
 
+    @JvmStatic
+    fun logException(logger: Logger, t: Throwable, message: () -> String) {
+        if (logger.isWarnEnabled) logger.warn(message(), t)
+    }
+
     @JvmStatic
     fun logException(logger: Logger, t: Throwable) {
         if (logger.isWarnEnabled) logger.warn(t.toString(), t)