Mapbox Android Offline Tile Caching: Architecture and Operational Boundaries
Architecture note on Mapbox Android offline tile caching: MBTiles storage boundaries, minimal region configuration, quota enforcement differences across Android versions, SQLite locking risks, and design pivots for integrity verification or background downloads.
05 Dec 2025, 23:38 UTC

Requirements: Why Offline Tiles Matter
Mobile mapping applications need reliable map rendering when network connectivity is intermittent or absent. The Mapbox Maps SDK for Android (v11.x) addresses this through an offline tile caching subsystem built on MBTiles — a SQLite-based container format that stores vector tiles with automatic gzip compression. The core requirement is deterministic tile availability within a defined geographic region and zoom range, without relying on runtime network requests.
Typical use cases include field data collection apps, navigation in remote areas, and reducing data costs for fleet tracking. The SDK exposes this through OfflineManager, RegionDefinition, and ResourceOptions APIs, but the architectural boundaries between application logic and SDK internals are where operational risk concentrates.
Smallest Suitable Design: Minimal Offline Region
A production-ready offline implementation starts with a single region definition, explicit storage quota, and a download observer. The minimal viable configuration:
// Define region: bounding box + zoom range + style URL
val region = RegionDefinition(
geometry = boundingBox.toGeometry(),
minZoom = 10.0,
maxZoom = 16.0,
styleUrl = "mapbox://styles/mapbox/streets-v12",
pixelRatio = resources.displayMetrics.density
)
// Configure cache limits before initializing OfflineManager
val cacheOptions = CacheOptions.Builder()
.maximumSize(500 * 1024 * 1024L) // 500 MB
.minimumSize(50 * 1024 * 1024L) // 50 MB floor
.build()
ResourceOptions.setCacheOptions(cacheOptions)
// Start download with progress callback
val offlineManager = OfflineManager.getInstance(context)
offlineManager.createRegion(region, object : OfflineManager.CreateRegionCallback {
override fun onCreate(offlineRegion: OfflineRegion) {
offlineRegion.setDownloadState(OfflineRegion.STATE_ACTIVE)
offlineRegion.setObserver(object : OfflineRegion.Observer {
override fun onStatusChanged(status: OfflineRegionStatus) {
// Handle progress, completion, errors
}
})
}
override fun onError(error: String) { /* handle region creation failure */ }
})
This design assumes the application controls region lifecycle (create, update, delete) while the SDK manages tile fetching, SQLite writes, and cache eviction. The pixelRatio parameter must match the device's display density; mismatched values cause redundant downloads for the same visual content.
Trust and Data Boundaries
The trust boundary splits at the OfflineRegion interface:
- Application owns: Region geometry, zoom constraints, style URL selection, download scheduling, retry logic, and user-facing progress UI.
- SDK owns: Tile request sequencing (max 5 concurrent connections), MBTiles schema management, gzip decompression, cache eviction order (LRU by last access), and SQLite transaction boundaries.
Critically, the application cannot inspect individual tile integrity inside the MBTiles database. The SDK does not expose tile-level checksums or corruption detection. If a tile fails to render, the only SDK-provided signal is a missing-tile callback during map rendering — not during download.
Storage location is another boundary. On Android 10+ (API 29), offline databases reside in the app's scoped storage (getExternalFilesDir()). On API 28 and below, they may write to shared external storage requiring MANAGE_EXTERNAL_STORAGE permission. The SDK handles this transparently, but quota enforcement behavior differs: scoped storage enforces per-app limits; shared storage competes with system-wide free space.
Operational Checks: Verifying Cache Health
Three diagnostic approaches validate the offline subsystem without modifying state:
1. Region Status Inspection
// Query all regions and their download status
val regions = offlineManager.listRegions()
regions.forEach { region ->
val status = region.getStatus()
Log.d("Offline", "Region: ${region.getDefinition()}, " +
"completed: ${status.getCompletedResourceCount()}/${status.getRequiredResourceCount()}, " +
"size: ${status.getCompletedResourceSize()} bytes")
}
Run this on the main thread after OfflineManager initialization. A region stuck at 0% completion with STATE_ACTIVE often indicates network permission issues or style URL mismatch.
2. Filesystem Quota Verification
// Check actual disk usage vs configured quota
val cacheDir = context.getExternalFilesDir(null)?.parentFile
val mbtilesFiles = cacheDir?.listFiles { it.extension == "mbtiles" }
val totalSize = mbtilesFiles?.sumOf { it.length() } ?: 0L
Log.d("Offline", "MBTiles total: $totalSize bytes, quota: ${cacheOptions.maximumSize}")
Execute after a download completes. If totalSize exceeds maximumSize, eviction is not functioning — possibly due to concurrent downloads holding SQLite locks.
3. Network Profiler Correlation
In Android Studio's Network Profiler, filter for api.mapbox.com during a region download. Verify:
- Maximum 5 concurrent TCP connections to
tiles.mapbox.comorapi.mapbox.com - Request headers include
Accept-Encoding: gzip - Response
Content-Encoding: gzipfor vector tiles
Deviations indicate SDK version mismatch or proxy interference.
Failure Modes and Detection
SQLite Database Locking During App Termination
If the process dies while OfflineManager holds a write transaction on the MBTiles database, the SQLite file can enter a locked state. On next app start, createRegion or listRegions throws SQLiteDatabaseLockedException.
Detection: Wrap region operations in try-catch for SQLiteException with error code 5 (SQLITE_BUSY). Recovery: Delete the corrupted .mbtiles file and re-download the region. The SDK does not self-heal.
Storage Quota Exhaustion
When maximumSize is reached, the SDK attempts LRU eviction of least-recently-accessed regions. However, eviction only triggers on new downloads — not on app startup. A region larger than the quota will download until disk full, then fail with OfflineRegion.ERROR_STORAGE.
Check: Monitor OfflineRegionStatus.getCompletedResourceSize() approaching maximumSize. Mitigation: Set maximumSize at least 20% above the largest single region's expected size.
Network Interruption Without Auto-Resume
The SDK provides no automatic resume. A download interrupted by connectivity loss leaves the region in STATE_ACTIVE with partial progress. Calling setDownloadState(STATE_ACTIVE) again restarts from the beginning.
Workaround: Persist OfflineRegionStatus.getCompletedResourceCount() to SharedPreferences. On retry, compare against getRequiredResourceCount(); if unchanged, the region may be complete but unverified. Force a style reload to trigger tile validation.
Style Version Drift
Offline regions bind to a specific style URL (e.g., mapbox://styles/mapbox/streets-v12). If Mapbox retires that style version, the region becomes unusable — tiles exist but reference missing sprite/glyph resources. The SDK does not warn on style deprecation.
Detection: Periodically query the Styles API for version status. Migration: Create a new region with the updated style URL; delete the old region after verification.
Conditions That Would Change the Design
| Condition | Design Change |
|---|---|
| Need tile-level integrity verification | Add application-layer checksums: download tile manifest separately, compute SHA-256 per tile, store in local DB. Increases storage ~5%. |
| Background downloads with WorkManager | Wrap OfflineManager calls in CoroutineWorker with setForeground() for progress notifications. Requires handling OfflineManager singleton lifecycle across process restarts. |
| Multiple style variants per region (day/night) | Create separate regions per style URL. Share geometry definition. Accept 2x storage. No SDK support for style fallbacks within one region. |
| Pre-seeded offline bundles (side-loaded) | Use OfflineManager.createRegion() with pre-populated MBTiles file placed in cache dir before first launch. Requires matching SDK MBTiles schema version. Undocumented — test per SDK release. |
| Dynamic zoom adjustment based on storage pressure | Implement custom eviction: monitor StorageManager callbacks, delete highest-zoom regions first via offlineManager.deleteRegion(). SDK LRU does not consider zoom level. |
Practical Verification Checklist
- After initial download: verify region status shows 100% completion and file size > 0.
- Kill app process mid-download (via
adb shell am kill); restart and confirm region recovers or cleanly fails. - Fill device storage to 95%; attempt new region download — expect
ERROR_STORAGE, not crash. - Switch style URL to deprecated version; confirm tiles render but sprites/glyphs fail (visible as missing icons/labels).
- Run Network Profiler during concurrent 5-region download; confirm connection limit holds.
These checks require physical device or emulator with controllable network/storage — not unit tests. The SDK's offline behavior is tightly coupled to Android's storage and networking subsystems.
Version Assumptions
This architecture note assumes Mapbox Maps SDK for Android v11.0.0–11.14.x. The OfflineManager API surface changed significantly in v10 (legacy OfflineTilePyramidRegionDefinition) and may change again in v12. Always verify RegionDefinition constructor parameters and CacheOptions builder methods against the specific SDK version's Javadoc.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.