๐Ÿง  Android AI Projects and Production

Fallbacks When AI Is Unavailable

AI services fail in more ways than typical APIs: rate limits, quota exhaustion, safety blocks, slow responses, unsupported devices, model deprecations. An AI feature without fallbacks becomes a broken feature on a bad day. Here's how to degrade gracefully.

โœ๏ธ Yunok Tech ๐Ÿ“… Updated โฑ 3 min read ๐ŸŽฏ Intermediate
On this page
  1. Failure types
  2. A fallback chain
  3. Implementing it
  4. Kill switches and remote config
  5. Failure UX
  6. Frequently asked questions
  7. Conclusion

The core of your app should never depend on an AI call succeeding. Design every AI feature with a plan for when the model is slow, refuses, errors or isn't available, and make that plan visible to users in a helpful way.

Failure types

FailureTypical signalGood response
Offline / network errorIOExceptionOn-device tier, queue, or offline message
Rate limited429Backoff and retry once; then explain
Quota exhaustedQuota error / your backend's codeExplain limit; offer non-AI path
Slow responseTimeoutCancel, offer retry or smaller model
Safety block / refusalBlocked finish reasonNeutral message; let user rephrase
Invalid outputJSON parse / validation failsRetry once; then fallback
Unsupported deviceFeature status UNAVAILABLECloud tier or hide feature
Model deprecated404 / model not foundRemote-config model name; alerting

A fallback chain

AI fallback chain Try the primary model with a timeout. On a retryable failure, retry once with backoff. If that fails, try a cheaper or on-device model. If that fails, return a cached result if one exists. Otherwise show the non-AI experience with a clear message. Primary+ timeout Retryonce, backoff Secondarymodel/tier Cacheif available Non-AIexperience Not every failure should walk the whole chain: safety blocks shouldn't be retried elsewhere.
Decide per failure type which steps make sense.

Implementing it

ResilientSummarizer.ktKotlin
sealed interface AiOutcome<out T> {
    data class Ok<T>(val value: T, val source: String) : AiOutcome<T>
    data class Unavailable(val reason: Reason) : AiOutcome<Nothing>
    enum class Reason { OFFLINE, LIMITED, BLOCKED, FAILED }
}

class ResilientSummarizer(
    private val primary: Summarizer,      // e.g. cloud Flash
    private val secondary: Summarizer?,   // e.g. on-device, or a lighter model
    private val cache: SummaryCache,
    private val flags: AiFlags,           // remote config kill switch, model names
) {
    suspend fun summarize(key: String, text: String): AiOutcome<String> {
        if (!flags.summariesEnabled) return AiOutcome.Unavailable(AiOutcome.Reason.FAILED)
        cache.get(key)?.let { return AiOutcome.Ok(it, "cache") }

        val first = attempt(primary, text, retries = 1)
        if (first is AttemptResult.Success) return first.save(key, "primary")
        if (first is AttemptResult.Blocked) return AiOutcome.Unavailable(AiOutcome.Reason.BLOCKED)

        secondary?.let { s ->
            val second = attempt(s, text, retries = 0)
            if (second is AttemptResult.Success) return second.save(key, "secondary")
        }
        return AiOutcome.Unavailable(if (first is AttemptResult.Offline) AiOutcome.Reason.OFFLINE else AiOutcome.Reason.LIMITED)
    }

    private suspend fun attempt(s: Summarizer, text: String, retries: Int): AttemptResult {
        repeat(retries + 1) { i ->
            val r = try {
                withTimeout(15_000) { AttemptResult.Success(s.summarize(text)) }
            } catch (e: TimeoutCancellationException) { AttemptResult.Failed
            } catch (e: IOException) { AttemptResult.Offline
            } catch (e: BlockedException) { return AttemptResult.Blocked
            } catch (e: RateLimitedException) { AttemptResult.Failed }
            if (r is AttemptResult.Success) return r
            if (i < retries) delay(1_000L * (i + 1) + Random.nextLong(250))     // backoff with jitter
        }
        return AttemptResult.Failed
    }

    private suspend fun AttemptResult.Success.save(key: String, source: String): AiOutcome<String> {
        cache.put(key, value); return AiOutcome.Ok(value, source)
    }
}
// AttemptResult (Success/Failed/Offline/Blocked) and the exceptions map from your SDK's errors.

Kill switches and remote config

  • Put feature enablement and model names in Remote Config, so you can disable a feature or switch models without an app release.
  • Use per-feature flags, so one broken feature doesn't take down others.
  • Set backend-side circuit breakers to stop calling a failing provider for a while.

Failure UX

  • Keep the non-AI path first-class: manual editing, regular search, templates.
  • Be specific: "You're offline. Summaries need internet on this device." is better than "Something went wrong".
  • Preserve user input on failure, and keep partial streamed results.
  • Don't blame the user for safety blocks. Suggest rephrasing neutrally.

Frequently asked questions

Should I fall back from one provider to another?

It can improve availability, but it adds complexity, different outputs and privacy implications (another provider's terms). Do it on the backend, if at all, and disclose it.

How many retries?

Usually one, with backoff, for transient errors. More retries add latency and cost, and can worsen rate limiting.

Conclusion

Treat AI as an optional enhancement with a plan B: classify failures, use timeouts and limited retries, a fallback chain (secondary tier, cache, non-AI), kill switches via remote config, and clear, input-preserving failure UX.

Next step: design trustworthy AI interfaces in Responsible AI User Experience.

Official references

๐Ÿ“ฑ Apps built by Yunok Tech

Real Android apps on Google Play, free to download.

See all our apps โ†’

Ready to build real Android applications?

Every AI feature still runs inside an Android app that has to be fast, reliable and well structured. Strengthen the Kotlin, Jetpack Compose and architecture skills that AI integrations depend on, then apply these AI concepts with confidence.

Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.

Explore the Android course and take your next step as an Android developer.

Share: LinkedIn X / Twitter WhatsApp