diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/retention/ContactDiaryWorkScheduler.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/retention/ContactDiaryWorkScheduler.kt
index d989e8146ceca7b951e43f8b005ad9c3a20e1108..15bbc81bdc3ca0ba7302bdecb7440d01772035d9 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/retention/ContactDiaryWorkScheduler.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/retention/ContactDiaryWorkScheduler.kt
@@ -3,6 +3,7 @@ package de.rki.coronawarnapp.contactdiary.retention
 import androidx.work.ExistingPeriodicWorkPolicy
 import androidx.work.WorkManager
 import dagger.Reusable
+import timber.log.Timber
 import javax.inject.Inject
 
 @Reusable
@@ -16,6 +17,7 @@ class ContactDiaryWorkScheduler @Inject constructor(
      * Replace with new if older work exists.
      */
     fun schedulePeriodic() {
+        Timber.d("ContactDiaryWorkScheduler schedulePeriodic()")
         // Create unique work and enqueue
         workManager.enqueueUniquePeriodicWork(
             PERIODIC_WORK_NAME,
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/Analytics.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/Analytics.kt
index c82bb2fd229c62772a1e4765b7c396afddb9992b..dab8333ca7fc18e204efa713891a1fad97f1b70f 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/Analytics.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/Analytics.kt
@@ -9,13 +9,16 @@ import de.rki.coronawarnapp.datadonation.analytics.server.DataDonationAnalyticsS
 import de.rki.coronawarnapp.datadonation.analytics.storage.AnalyticsSettings
 import de.rki.coronawarnapp.datadonation.analytics.storage.LastAnalyticsSubmissionLogger
 import de.rki.coronawarnapp.datadonation.safetynet.DeviceAttestation
+import de.rki.coronawarnapp.datadonation.safetynet.SafetyNetException
 import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpaData
 import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpaDataRequestAndroid
 import de.rki.coronawarnapp.storage.LocalData
 import de.rki.coronawarnapp.util.TimeStamper
+import kotlinx.coroutines.TimeoutCancellationException
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.sync.Mutex
 import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withTimeout
 import org.joda.time.Hours
 import org.joda.time.Instant
 import timber.log.Timber
@@ -36,36 +39,36 @@ class Analytics @Inject constructor(
 ) {
     private val submissionLockoutMutex = Mutex()
 
-    private suspend fun trySubmission(analyticsConfig: AnalyticsConfig, ppaData: PpaData.PPADataAndroid): Boolean {
-        try {
+    private suspend fun trySubmission(analyticsConfig: AnalyticsConfig, ppaData: PpaData.PPADataAndroid): Result {
+        return try {
             val ppaAttestationRequest = PPADeviceAttestationRequest(
                 ppaData = ppaData
             )
 
-            Timber.d("Starting safety net device attestation")
+            Timber.tag(TAG).d("Starting safety net device attestation")
 
             val attestation = deviceAttestation.attest(ppaAttestationRequest)
 
             attestation.requirePass(analyticsConfig.safetyNetRequirements)
 
-            Timber.d("Safety net attestation passed")
+            Timber.tag(TAG).d("Safety net attestation passed")
 
             val ppaContainer = PpaDataRequestAndroid.PPADataRequestAndroid.newBuilder()
                 .setAuthentication(attestation.accessControlProtoBuf)
                 .setPayload(ppaData)
                 .build()
 
-            Timber.d("Starting analytics upload")
+            Timber.tag(TAG).d("Starting analytics upload")
 
             dataDonationAnalyticsServer.uploadAnalyticsData(ppaContainer)
 
-            Timber.d("Analytics upload finished")
+            Timber.tag(TAG).d("Analytics upload finished")
 
-            return true
+            Result(successful = true)
         } catch (err: Exception) {
-            Timber.e(err, "Error during analytics submission")
             err.reportProblem(tag = TAG, info = "An error occurred during analytics submission")
-            return false
+            val retry = err is SafetyNetException && err.type == SafetyNetException.Type.ATTESTATION_REQUEST_FAILED
+            Result(successful = false, shouldRetry = retry)
         }
     }
 
@@ -74,7 +77,7 @@ class Analytics @Inject constructor(
 
         val contributions = donorModules.mapNotNull {
             val moduleName = it::class.simpleName
-            Timber.d("Beginning donation for module: %s", moduleName)
+            Timber.tag(TAG).d("Beginning donation for module: %s", moduleName)
             try {
                 it.beginDonation(request)
             } catch (e: Exception) {
@@ -85,7 +88,7 @@ class Analytics @Inject constructor(
 
         contributions.forEach {
             val moduleName = it::class.simpleName
-            Timber.d("Injecting contribution: %s", moduleName)
+            Timber.tag(TAG).d("Injecting contribution: %s", moduleName)
             try {
                 it.injectData(ppaDataBuilder)
             } catch (e: Exception) {
@@ -97,8 +100,8 @@ class Analytics @Inject constructor(
     }
 
     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-    suspend fun submitAnalyticsData(analyticsConfig: AnalyticsConfig) {
-        Timber.d("Starting analytics submission")
+    suspend fun submitAnalyticsData(analyticsConfig: AnalyticsConfig): Result {
+        Timber.tag(TAG).d("Starting analytics submission")
 
         val ppaDataBuilder = PpaData.PPADataAndroid.newBuilder()
 
@@ -106,19 +109,28 @@ class Analytics @Inject constructor(
 
         val analyticsProto = ppaDataBuilder.build()
 
-        val success = trySubmission(analyticsConfig, analyticsProto)
+        val result = try {
+            // 6min, if attestation and/or submission takes longer than that,
+            // then we want to give modules still time to cleanup and get into a consistent state.
+            withTimeout(360_000) {
+                trySubmission(analyticsConfig, analyticsProto)
+            }
+        } catch (e: TimeoutCancellationException) {
+            Timber.tag(TAG).e(e, "trySubmission() timed out after 360s.")
+            Result(successful = false, shouldRetry = true)
+        }
 
         contributions.forEach {
             val moduleName = it::class.simpleName
-            Timber.d("Finishing contribution: %s", moduleName)
+            Timber.tag(TAG).d("Finishing contribution($result) for %s", moduleName)
             try {
-                it.finishDonation(success)
+                it.finishDonation(result.successful)
             } catch (e: Exception) {
-                e.reportProblem(TAG, "finishDonation($success): $moduleName failed")
+                e.reportProblem(TAG, "finishDonation($result): $moduleName failed")
             }
         }
 
-        if (success) {
+        if (result.successful) {
             settings.lastSubmittedTimestamp.update {
                 timeStamper.nowUTC
             }
@@ -126,7 +138,8 @@ class Analytics @Inject constructor(
             logger.storeAnalyticsData(analyticsProto)
         }
 
-        Timber.d("Finished analytics submission success=%s", success)
+        Timber.tag(TAG).d("Finished analytics submission result=%s", result)
+        return result
     }
 
     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
@@ -158,36 +171,36 @@ class Analytics @Inject constructor(
         return onboarding.plus(Hours.hours(ONBOARDING_DELAY_HOURS).toStandardDuration()).isAfter(timeStamper.nowUTC)
     }
 
-    suspend fun submitIfWanted() = submissionLockoutMutex.withLock {
-        Timber.d("Checking analytics conditions")
+    suspend fun submitIfWanted(): Result = submissionLockoutMutex.withLock {
+        Timber.tag(TAG).d("Checking analytics conditions")
         val analyticsConfig: AnalyticsConfig = appConfigProvider.getAppConfig().analytics
 
         if (stopDueToNoAnalyticsConfig(analyticsConfig)) {
-            Timber.w("Aborting Analytics submission due to noAnalyticsConfig")
-            return
+            Timber.tag(TAG).w("Aborting Analytics submission due to noAnalyticsConfig")
+            return@withLock Result(successful = false)
         }
 
         if (stopDueToNoUserConsent()) {
-            Timber.w("Aborting Analytics submission due to noUserConsent")
-            return
+            Timber.tag(TAG).w("Aborting Analytics submission due to noUserConsent")
+            return@withLock Result(successful = false)
         }
 
         if (stopDueToProbabilityToSubmit(analyticsConfig)) {
-            Timber.w("Aborting Analytics submission due to probabilityToSubmit")
-            return
+            Timber.tag(TAG).w("Aborting Analytics submission due to probabilityToSubmit")
+            return@withLock Result(successful = false)
         }
 
         if (stopDueToLastSubmittedTimestamp()) {
-            Timber.w("Aborting Analytics submission due to lastSubmittedTimestamp")
-            return
+            Timber.tag(TAG).w("Aborting Analytics submission due to lastSubmittedTimestamp")
+            return@withLock Result(successful = false)
         }
 
         if (stopDueToTimeSinceOnboarding()) {
-            Timber.w("Aborting Analytics submission due to timeSinceOnboarding")
-            return
+            Timber.tag(TAG).w("Aborting Analytics submission due to timeSinceOnboarding")
+            return@withLock Result(successful = false)
         }
 
-        submitAnalyticsData(analyticsConfig)
+        return@withLock submitAnalyticsData(analyticsConfig)
     }
 
     private suspend fun deleteAllData() = submissionLockoutMutex.withLock {
@@ -209,6 +222,11 @@ class Analytics @Inject constructor(
     fun isAnalyticsEnabledFlow(): Flow<Boolean> =
         settings.analyticsEnabled.flow
 
+    data class Result(
+        val successful: Boolean,
+        val shouldRetry: Boolean = false
+    )
+
     companion object {
         private val TAG = Analytics::class.java.simpleName
         private const val LAST_SUBMISSION_MIN_AGE_HOURS = 23
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/worker/DataDonationAnalyticsPeriodicWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/worker/DataDonationAnalyticsPeriodicWorker.kt
index c964f618cc1c50400fa516c0a7a6abc646df42c6..0920f50625cd7cc07b9d2262d701b3e7a82b5716 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/worker/DataDonationAnalyticsPeriodicWorker.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/worker/DataDonationAnalyticsPeriodicWorker.kt
@@ -20,8 +20,7 @@ class DataDonationAnalyticsPeriodicWorker @AssistedInject constructor(
     @Assisted context: Context,
     @Assisted workerParams: WorkerParameters,
     private val analytics: Analytics
-) :
-    CoroutineWorker(context, workerParams) {
+) : CoroutineWorker(context, workerParams) {
 
     override suspend fun doWork(): Result {
         Timber.tag(TAG).d("Background job started. Run attempt: $runAttemptCount")
@@ -31,15 +30,19 @@ class DataDonationAnalyticsPeriodicWorker @AssistedInject constructor(
 
             return Result.failure()
         }
-        var result = Result.success()
-        try {
-            analytics.submitIfWanted()
+
+        return try {
+            val analyticsResult = analytics.submitIfWanted()
+            Timber.tag(TAG).d("submitIfWanted() finished: %s", analyticsResult)
+            when {
+                analyticsResult.successful -> Result.success()
+                analyticsResult.shouldRetry -> Result.retry()
+                else -> Result.failure()
+            }
         } catch (e: Exception) {
-            Timber.tag(TAG).d(e)
-            result = Result.retry()
+            Timber.tag(TAG).w(e, "submitIfWanted() failed unexpectedly")
+            Result.failure()
         }
-
-        return result
     }
 
     @AssistedFactory
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/worker/DataDonationAnalyticsScheduler.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/worker/DataDonationAnalyticsScheduler.kt
index 3b556a0025730a7471f0d34fa29adcfd9da83c14..d4442e6890d606c91786ee01a9673fff21a141a1 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/worker/DataDonationAnalyticsScheduler.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/worker/DataDonationAnalyticsScheduler.kt
@@ -3,6 +3,7 @@ package de.rki.coronawarnapp.datadonation.analytics.worker
 import androidx.work.ExistingPeriodicWorkPolicy
 import androidx.work.WorkManager
 import dagger.Reusable
+import timber.log.Timber
 import javax.inject.Inject
 
 /**
@@ -23,7 +24,8 @@ class DataDonationAnalyticsScheduler @Inject constructor(
      */
     fun schedulePeriodic() {
         val initialDelay = timeCalculation.getDelay()
-
+        // TODO Replace with logic that checks if already scheduled workManager. workManager.getWorkInfosByTag()
+        Timber.d("schedulePeriodic() initialDelay(if not yet scheduled)=%s", initialDelay)
         // Create unique work and enqueue
         workManager.enqueueUniquePeriodicWork(
             PERIODIC_WORK_NAME,
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/SafetyNetClientWrapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/SafetyNetClientWrapper.kt
index 9cc34acaa7d66707d69e48d56535881d1546c632..f1b732d2640e529df3d79d7667492bf8c58081a9 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/SafetyNetClientWrapper.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/SafetyNetClientWrapper.kt
@@ -10,6 +10,7 @@ import dagger.Reusable
 import de.rki.coronawarnapp.datadonation.safetynet.SafetyNetException.Type
 import de.rki.coronawarnapp.environment.EnvironmentSetup
 import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.suspendCancellableCoroutine
 import kotlinx.coroutines.withTimeout
 import okio.ByteString
 import okio.ByteString.Companion.decodeBase64
@@ -17,7 +18,6 @@ import timber.log.Timber
 import javax.inject.Inject
 import kotlin.coroutines.resume
 import kotlin.coroutines.resumeWithException
-import kotlin.coroutines.suspendCoroutine
 
 @Reusable
 class SafetyNetClientWrapper @Inject constructor(
@@ -27,9 +27,9 @@ class SafetyNetClientWrapper @Inject constructor(
 
     suspend fun attest(nonce: ByteArray): Report {
         val response = try {
-            withTimeout(30 * 1000L) { callClient(nonce) }
+            withTimeout(180_000) { callClient(nonce) }
         } catch (e: TimeoutCancellationException) {
-            throw SafetyNetException(Type.ATTESTATION_FAILED, "Attestation timeout.", e)
+            throw SafetyNetException(Type.ATTESTATION_REQUEST_FAILED, "Attestation timeout (us).", e)
         }
 
         val jwsResult = response.jwsResult ?: throw SafetyNetException(
@@ -73,22 +73,46 @@ class SafetyNetClientWrapper @Inject constructor(
         return JsonParser.parseString(rawJson).asJsonObject
     }
 
-    private suspend fun callClient(nonce: ByteArray): SafetyNetApi.AttestationResponse = suspendCoroutine { cont ->
-        safetyNetClient.attest(nonce, environmentSetup.safetyNetApiKey)
-            .addOnSuccessListener {
-                Timber.tag(TAG).v("Attestation finished with %s", it)
-                cont.resume(it)
-            }
-            .addOnFailureListener {
-                Timber.tag(TAG).w(it, "Attestation failed.")
-                val wrappedError = if (it is ApiException && it.statusCode == CommonStatusCodes.NETWORK_ERROR) {
-                    SafetyNetException(Type.ATTESTATION_REQUEST_FAILED, "Network error", it)
-                } else {
-                    SafetyNetException(Type.ATTESTATION_FAILED, "SafetyNet client returned an error.", it)
+    private suspend fun callClient(nonce: ByteArray): SafetyNetApi.AttestationResponse =
+        suspendCancellableCoroutine { cont ->
+            safetyNetClient.attest(nonce, environmentSetup.safetyNetApiKey)
+                .addOnSuccessListener {
+                    Timber.tag(TAG).v("Attestation finished with %s", it)
+                    cont.resume(it)
                 }
-                cont.resumeWithException(wrappedError)
-            }
-    }
+                .addOnFailureListener {
+                    Timber.tag(TAG).w(it, "Attestation failed.")
+                    val defaultError =
+                        SafetyNetException(Type.ATTESTATION_FAILED, "SafetyNet client returned an error.", it)
+
+                    if (it !is ApiException) {
+                        cont.resumeWithException(defaultError)
+                        return@addOnFailureListener
+                    }
+
+                    val apiError = when (it.statusCode) {
+                        CommonStatusCodes.TIMEOUT -> SafetyNetException(
+                            Type.ATTESTATION_REQUEST_FAILED,
+                            "Timeout (them)",
+                            it
+                        )
+                        // com.google.android.gms.common.api.ApiException: 20:
+                        // The connection to Google Play services was lost due to service disconnection.
+                        // Last reason for disconnect: Timing out service connection.
+                        CommonStatusCodes.CONNECTION_SUSPENDED_DURING_CALL,
+                        CommonStatusCodes.RECONNECTION_TIMED_OUT_DURING_UPDATE,
+                        CommonStatusCodes.RECONNECTION_TIMED_OUT,
+                        CommonStatusCodes.NETWORK_ERROR -> SafetyNetException(
+                            Type.ATTESTATION_REQUEST_FAILED,
+                            "Network error (${it.statusCode})",
+                            it
+                        )
+                        else -> defaultError
+                    }
+
+                    cont.resumeWithException(apiError)
+                }
+        }
 
     data class Report(
         val jwsResult: String,
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/AnalyticsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/AnalyticsTest.kt
index c8f645bdb07ae80b7059cc2b30d525da0f1ee8d3..306ff85627dfbb7a37afd3a54ae02c870bd3f07c 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/AnalyticsTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/AnalyticsTest.kt
@@ -12,11 +12,13 @@ import de.rki.coronawarnapp.datadonation.analytics.server.DataDonationAnalyticsS
 import de.rki.coronawarnapp.datadonation.analytics.storage.AnalyticsSettings
 import de.rki.coronawarnapp.datadonation.analytics.storage.LastAnalyticsSubmissionLogger
 import de.rki.coronawarnapp.datadonation.safetynet.DeviceAttestation
+import de.rki.coronawarnapp.datadonation.safetynet.SafetyNetException
 import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpaData
 import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpaDataRequestAndroid
 import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpacAndroid
 import de.rki.coronawarnapp.storage.LocalData
 import de.rki.coronawarnapp.util.TimeStamper
+import io.kotest.matchers.shouldBe
 import io.mockk.MockKAnnotations
 import io.mockk.Runs
 import io.mockk.clearAllMocks
@@ -28,6 +30,7 @@ import io.mockk.just
 import io.mockk.mockk
 import io.mockk.mockkObject
 import io.mockk.spyk
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.test.runBlockingTest
 import org.joda.time.Days
 import org.joda.time.Instant
@@ -102,7 +105,11 @@ class AnalyticsTest : BaseTest() {
         val analytics = createInstance()
 
         runBlockingTest2 {
-            analytics.submitIfWanted()
+            val result = analytics.submitIfWanted()
+            result.apply {
+                successful shouldBe false
+                shouldRetry shouldBe false
+            }
         }
 
         coVerify(exactly = 1) {
@@ -122,7 +129,11 @@ class AnalyticsTest : BaseTest() {
         val analytics = createInstance()
 
         runBlockingTest2 {
-            analytics.submitIfWanted()
+            val result = analytics.submitIfWanted()
+            result.apply {
+                successful shouldBe false
+                shouldRetry shouldBe false
+            }
         }
 
         coVerify(exactly = 1) {
@@ -143,7 +154,11 @@ class AnalyticsTest : BaseTest() {
         val analytics = createInstance()
 
         runBlockingTest2 {
-            analytics.submitIfWanted()
+            val result = analytics.submitIfWanted()
+            result.apply {
+                successful shouldBe false
+                shouldRetry shouldBe false
+            }
         }
 
         coVerify(exactly = 1) {
@@ -165,7 +180,11 @@ class AnalyticsTest : BaseTest() {
         val analytics = createInstance()
 
         runBlockingTest2 {
-            analytics.submitIfWanted()
+            val result = analytics.submitIfWanted()
+            result.apply {
+                successful shouldBe false
+                shouldRetry shouldBe false
+            }
         }
 
         coVerify(exactly = 1) {
@@ -188,7 +207,11 @@ class AnalyticsTest : BaseTest() {
         val analytics = createInstance()
 
         runBlockingTest2 {
-            analytics.submitIfWanted()
+            val result = analytics.submitIfWanted()
+            result.apply {
+                successful shouldBe false
+                shouldRetry shouldBe false
+            }
         }
 
         coVerify(exactly = 1) {
@@ -239,7 +262,11 @@ class AnalyticsTest : BaseTest() {
         val analytics = createInstance()
 
         runBlockingTest2 {
-            analytics.submitIfWanted()
+            val result = analytics.submitIfWanted()
+            result.apply {
+                successful shouldBe true
+                shouldRetry shouldBe false
+            }
         }
 
         coVerify(exactly = 1) {
@@ -327,4 +354,105 @@ class AnalyticsTest : BaseTest() {
             analytics.submitAnalyticsData(any())
         }
     }
+
+    @Test
+    fun `we catch safetynet timeout and enable retry`() {
+        val exposureRiskDonation = mockk<ExposureRiskMetadataDonor.ExposureRiskMetadataContribution>().apply {
+            coEvery { injectData(any()) } just Runs
+            coEvery { finishDonation(any()) } just Runs
+        }
+        coEvery { exposureRiskMetadataDonor.beginDonation(any()) } returns exposureRiskDonation
+
+        coEvery { deviceAttestation.attest(any()) } throws SafetyNetException(
+            type = SafetyNetException.Type.ATTESTATION_REQUEST_FAILED,
+            "Timeout???",
+            cause = Exception()
+        )
+
+        val analytics = createInstance()
+
+        runBlockingTest {
+            val result = analytics.submitIfWanted()
+            result.successful shouldBe false
+            result.shouldRetry shouldBe true
+        }
+
+        coVerify(exactly = 1) {
+            exposureRiskMetadataDonor.beginDonation(any())
+            exposureRiskDonation.injectData(any())
+            exposureRiskDonation.finishDonation(false)
+        }
+
+        coVerify(exactly = 0) { dataDonationAnalyticsServer.uploadAnalyticsData(any()) }
+    }
+
+    @Test
+    fun `overall submission can timeout on safetynet and still allow modules to cleanup`() {
+        val exposureRiskDonation = mockk<ExposureRiskMetadataDonor.ExposureRiskMetadataContribution>().apply {
+            coEvery { injectData(any()) } just Runs
+            coEvery { finishDonation(any()) } just Runs
+        }
+        coEvery { exposureRiskMetadataDonor.beginDonation(any()) } returns exposureRiskDonation
+
+        coEvery { deviceAttestation.attest(any()) } coAnswers {
+            // Timeout should be 360s
+            delay(370_000)
+            mockk()
+        }
+
+        val analytics = createInstance()
+
+        runBlockingTest {
+            val result = analytics.submitIfWanted()
+            result.successful shouldBe false
+            result.shouldRetry shouldBe true
+        }
+
+        coVerify(exactly = 1) {
+            exposureRiskMetadataDonor.beginDonation(any())
+            exposureRiskDonation.injectData(any())
+            exposureRiskDonation.finishDonation(false)
+        }
+
+        coVerify(exactly = 0) {
+            dataDonationAnalyticsServer.uploadAnalyticsData(any())
+        }
+    }
+
+    @Test
+    fun `overall submission can timeout on upload and still allow modules to cleanup`() {
+        val exposureRiskDonation = mockk<ExposureRiskMetadataDonor.ExposureRiskMetadataContribution>().apply {
+            coEvery { injectData(any()) } just Runs
+            coEvery { finishDonation(any()) } just Runs
+        }
+        coEvery { exposureRiskMetadataDonor.beginDonation(any()) } returns exposureRiskDonation
+
+        coEvery { deviceAttestation.attest(any()) } returns object : DeviceAttestation.Result {
+            override val accessControlProtoBuf: PpacAndroid.PPACAndroid
+                get() = PpacAndroid.PPACAndroid.getDefaultInstance()
+
+            override fun requirePass(requirements: SafetyNetRequirements) {}
+        }
+
+        coEvery { dataDonationAnalyticsServer.uploadAnalyticsData(any()) } coAnswers {
+            // Timeout should be 360s
+            delay(370_000)
+            mockk()
+        }
+
+        val analytics = createInstance()
+
+        runBlockingTest {
+            val result = analytics.submitIfWanted()
+            result.successful shouldBe false
+            result.shouldRetry shouldBe true
+        }
+
+        coVerify(exactly = 1) {
+            exposureRiskMetadataDonor.beginDonation(any())
+            exposureRiskDonation.injectData(any())
+            exposureRiskDonation.finishDonation(false)
+            dataDonationAnalyticsServer.uploadAnalyticsData(any())
+        }
+    }
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/worker/DataDonationAnalyticsPeriodicWorkerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/worker/DataDonationAnalyticsPeriodicWorkerTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2ea4a82db16288a86dc944bad41944940386f3d6
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/worker/DataDonationAnalyticsPeriodicWorkerTest.kt
@@ -0,0 +1,70 @@
+package de.rki.coronawarnapp.datadonation.analytics.worker
+
+import android.content.Context
+import androidx.work.ListenableWorker
+import androidx.work.WorkerParameters
+import de.rki.coronawarnapp.datadonation.analytics.Analytics
+import de.rki.coronawarnapp.worker.BackgroundConstants
+import io.kotest.matchers.shouldBe
+import io.mockk.MockKAnnotations
+import io.mockk.clearAllMocks
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.impl.annotations.RelaxedMockK
+import kotlinx.coroutines.test.runBlockingTest
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+
+class DataDonationAnalyticsPeriodicWorkerTest : BaseTest() {
+
+    @MockK lateinit var context: Context
+    @MockK lateinit var analytics: Analytics
+    @RelaxedMockK lateinit var workerParams: WorkerParameters
+
+    @BeforeEach
+    fun setup() {
+        MockKAnnotations.init(this)
+    }
+
+    @AfterEach
+    fun teardown() {
+        clearAllMocks()
+    }
+
+    private fun createWorker() = DataDonationAnalyticsPeriodicWorker(
+        context = context,
+        workerParams = workerParams,
+        analytics = analytics
+    )
+
+    @Test
+    fun `if result says retry, do retry`() = runBlockingTest {
+        coEvery { analytics.submitIfWanted() } returns Analytics.Result(successful = false, shouldRetry = true)
+        createWorker().doWork() shouldBe ListenableWorker.Result.Retry()
+
+        coEvery { analytics.submitIfWanted() } returns Analytics.Result(successful = false, shouldRetry = false)
+        createWorker().doWork() shouldBe ListenableWorker.Result.Failure()
+
+        coEvery { analytics.submitIfWanted() } returns Analytics.Result(successful = true, shouldRetry = false)
+        createWorker().doWork() shouldBe ListenableWorker.Result.Success()
+    }
+
+    @Test
+    fun `maximum of 2 retry attemtps`() = runBlockingTest {
+        val worker = createWorker()
+        worker.runAttemptCount shouldBe 0
+
+        every { worker.runAttemptCount } returns BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD + 1
+
+        worker.doWork() shouldBe ListenableWorker.Result.Failure()
+    }
+
+    @Test
+    fun `unexpected errors do not cause a retry`() = runBlockingTest {
+        coEvery { analytics.submitIfWanted() } throws Exception("SURPRISE!!!")
+        createWorker().doWork() shouldBe ListenableWorker.Result.Failure()
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/safetynet/SafetyNetClientWrapperTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/safetynet/SafetyNetClientWrapperTest.kt
index a294c548f2549b445b0f788488d89f75633e22f5..71e072bfbea318f54b6664853f4cf32f4246345d 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/safetynet/SafetyNetClientWrapperTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/safetynet/SafetyNetClientWrapperTest.kt
@@ -24,7 +24,6 @@ import org.junit.jupiter.api.AfterEach
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
 import testhelpers.BaseTest
-import testhelpers.coroutines.runBlockingTest2
 import testhelpers.gms.MockGMSTask
 import java.io.IOException
 
@@ -74,7 +73,7 @@ class SafetyNetClientWrapperTest : BaseTest() {
     }
 
     @Test
-    fun `attestation can time out`() = runBlockingTest2(ignoreActive = true) {
+    fun `attestation can time out`() = runBlockingTest {
         every { safetyNetClient.attest(any(), any()) } returns MockGMSTask.timeout()
 
         val resultAsync = async {
@@ -83,10 +82,8 @@ class SafetyNetClientWrapperTest : BaseTest() {
             }
         }
 
-        advanceTimeBy(31 * 1000L)
-
         val error = resultAsync.await()
-        error.type shouldBe SafetyNetException.Type.ATTESTATION_FAILED
+        error.type shouldBe SafetyNetException.Type.ATTESTATION_REQUEST_FAILED
         error.cause shouldBe instanceOf(TimeoutCancellationException::class)
     }