diff --git a/app/src/main/java/io/xxlabs/messenger/repository/client/ClientRepository.kt b/app/src/main/java/io/xxlabs/messenger/repository/client/ClientRepository.kt
index 89d818a7267c3aac12eb19ed2cbb08f6d66bc98f..d12615f852ab51a74a5238f1285d2c7770a2db5e 100644
--- a/app/src/main/java/io/xxlabs/messenger/repository/client/ClientRepository.kt
+++ b/app/src/main/java/io/xxlabs/messenger/repository/client/ClientRepository.kt
@@ -501,6 +501,7 @@ class ClientRepository @Inject constructor(
         return Single.create { emitter ->
             try {
                 if (!areNodesReady()) {
+                    Timber.d("Failed to register-- nodes aren't ready.")
                     emitter.onError(throwNodeError())
                     return@create
                 }
@@ -512,6 +513,7 @@ class ClientRepository @Inject constructor(
                 exportUserContact()
                 emitter.onSuccess(username)
             } catch (e: Exception) {
+                Timber.d("Failed to register: ${e.message}")
                 emitter.onError(e)
             }
         }
@@ -959,7 +961,11 @@ class ClientRepository @Inject constructor(
         val status = try {
              clientWrapper.getNodeRegistrationStatus()
         } catch (e: Exception) {
-            if (e.isNodeError()) return false
+            return if (e.isNodeError()) {
+                Timber.d("Network is not healthy. Waiting $NODES_READY_POLL_INTERVAL before retry.")
+                Thread.sleep(NODES_READY_POLL_INTERVAL)
+                recursiveAreNodesReady()
+            }
             else throw e
         }
 
@@ -967,6 +973,10 @@ class ClientRepository @Inject constructor(
         Timber.v("[NODE REGISTRATION STATUS] Registration rate: $rate")
 
         return if (rate < NODES_READY_MINIMUM_RATE && retries <= NODES_READY_MAX_RETRIES) {
+            Timber.d(
+                "Nodes not ready after ${retries + 1} attempts. " +
+                        "Waiting $NODES_READY_POLL_INTERVAL before next retry."
+            )
             Thread.sleep(NODES_READY_POLL_INTERVAL)
             recursiveAreNodesReady(retries+1)
         } else {
diff --git a/app/src/main/java/io/xxlabs/messenger/ui/global/NetworkViewModel.kt b/app/src/main/java/io/xxlabs/messenger/ui/global/NetworkViewModel.kt
index 0b32da51779e892783997fcd2ea25fd10a55c59b..77eca506a04ec173ff389ba00420fa313dae6715 100644
--- a/app/src/main/java/io/xxlabs/messenger/ui/global/NetworkViewModel.kt
+++ b/app/src/main/java/io/xxlabs/messenger/ui/global/NetworkViewModel.kt
@@ -229,6 +229,7 @@ class NetworkViewModel @Inject constructor(
         Timber.v("[NETWORK VIEWMODEL] has network follower already started: $networkStatus")
         if (networkStatus == NetworkFollowerStatus.RUNNING) {
             checkStopNetworkTimer()
+            onStartCallback?.invoke(true)
         } else if (networkStatus == NetworkFollowerStatus.STOPPED) {
             startNetworkFollower(onStartCallback)
         }
@@ -265,8 +266,7 @@ class NetworkViewModel @Inject constructor(
                 .doOnSuccess {
                     Timber.v("[NETWORK VIEWMODEL] Network follower is RUNNING")
                     Timber.v("[NETWORK VIEWMODEL] Started network follower in: ${elapsedTime - System.currentTimeMillis()}ms")
-                    onStartCallback?.invoke(it)
-                    newUserDiscovery()
+                    newUserDiscovery(onStartCallback)
                 }
                 .doOnError { err ->
                     Timber.v("[NETWORK VIEWMODEL] Network follower ERROR - could not start properly: ${err.localizedMessage}")
@@ -371,7 +371,7 @@ class NetworkViewModel @Inject constructor(
         }
     }
 
-    fun newUserDiscovery() {
+    private fun newUserDiscovery(onCompleteCallback: ((Boolean) -> Unit)? = null) {
         if (!isUdTryingToRun && !isUserDiscoveryRunning()) {
             isUdTryingToRun = true
             Timber.v("Starting user discovery...")
@@ -383,13 +383,17 @@ class NetworkViewModel @Inject constructor(
                         Timber.e("[NETWORK VIEWMODEL] Failed to register user discovery: ${err.localizedMessage}")
                         isUdTryingToRun = false
                         userDiscoveryStatus.value = false
+                        onCompleteCallback?.invoke(false)
                     }.doOnSuccess {
                         Timber.v("[NETWORK VIEWMODEL] User discovery registered with success!")
                         setUserDiscoveryRunning()
                         isUdTryingToRun = false
                         userDiscoveryStatus.value = true
+                        onCompleteCallback?.invoke(true)
                     }.subscribe()
             )
+        } else {
+            onCompleteCallback?.invoke(true)
         }
     }
 
diff --git a/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/username/UsernameRegistration.kt b/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/username/UsernameRegistration.kt
index c6f4b42d11670a6bc5066a17ddf56ef6c0656885..d029f00f2e1eca695c6397b2f6037a46ca4bc697 100644
--- a/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/username/UsernameRegistration.kt
+++ b/app/src/main/java/io/xxlabs/messenger/ui/intro/registration/username/UsernameRegistration.kt
@@ -19,12 +19,19 @@ import io.xxlabs.messenger.bindings.wrapper.bindings.bindingsErrorMessage
 import io.xxlabs.messenger.repository.PreferencesRepository
 import io.xxlabs.messenger.repository.base.BaseRepository
 import io.xxlabs.messenger.support.appContext
+import io.xxlabs.messenger.support.util.value
 import io.xxlabs.messenger.ui.dialog.info.InfoDialogUI
 import io.xxlabs.messenger.ui.dialog.info.SpanConfig
 import io.xxlabs.messenger.ui.global.NetworkViewModel
 import kotlinx.coroutines.*
+import timber.log.Timber
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
 import kotlin.random.Random.Default.nextInt
 
+private const val MAX_NETWORK_RETRIES = 29
+private const val NETWORK_POLL_INTERVAL_MS = 1000L
+
 /**
  * Encapsulates username registration logic.
  */
@@ -230,7 +237,7 @@ class UsernameRegistration @AssistedInject constructor(
     }
 
     private fun getOrCreateSession(context: Context = appContext()) {
-        scope.launch {
+        scope.launch(Dispatchers.IO) {
             val appFolder = repo.createSessionFolder(context)
             try {
                 repo.newClient(appFolder, sessionPassword)
@@ -243,10 +250,25 @@ class UsernameRegistration @AssistedInject constructor(
         }
     }
 
-    private fun connectToCmix() {
-        with (networking) {
-            checkRegisterNetworkCallback()
-            tryStartNetworkFollower { onUsernameNextClicked() }
+    private suspend fun connectToCmix(retries: Int = 0) {
+        networking.checkRegisterNetworkCallback()
+        if (retries < MAX_NETWORK_RETRIES) {
+            if (initializeNetworkFollower()) {
+                Timber.d("Started network follower after #${retries + 1} attempt(s).")
+                withContext(Dispatchers.Main) {
+                    onUsernameNextClicked()
+                }
+            } else {
+                delay(NETWORK_POLL_INTERVAL_MS)
+                Timber.d("Attempting to start network follower, attempt #${retries + 1}.")
+                connectToCmix(retries + 1)
+            }
+        } else throw Exception("Failed to connect to network after ${retries + 1} attempts. Please try again.")
+    }
+
+    private suspend fun initializeNetworkFollower(): Boolean = suspendCoroutine { continuation ->
+        networking.tryStartNetworkFollower { successful ->
+            continuation.resume(successful)
         }
     }