Data Compression for Mobile Bandwidth Savings
Whistl minimises mobile data usage through intelligent compression. This technical guide explains gzip compression, Protocol Buffers, image optimisation, delta sync, and how Whistl reduces bandwidth by 85% compared to uncompressed APIs.
Why Compression Matters
Mobile data has real costs and limitations:
- Data caps: Many users have limited monthly data
- Slow networks: 3G/4G still common in many areas
- Roaming charges: International data is expensive
- Battery impact: Network activity drains battery
- Latency: Less data = faster transfers
Whistl's compression strategies reduce monthly data usage to under 50MB.
HTTP Compression (gzip)
All API responses are compressed with gzip:
Server Configuration
# Nginx configuration
http {
# Enable gzip
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
# Compress these content types
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml
application/xml+rss;
# Minimum length to compress
gzip_min_length 1000;
}
Client-Side Handling
import Foundation
class APIClient {
private let session: URLSession
func request(_ endpoint: String) async throws -> T {
var request = URLRequest(url: API.baseURL.appendingPathComponent(endpoint))
// Request compressed response
request.setValue("gzip, deflate", forHTTPHeaderField: "Accept-Encoding")
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw APIError.invalidResponse
}
// Handle compressed response
let decompressedData: Data
if httpResponse.value(forHTTPHeaderField: "Content-Encoding") == "gzip" {
decompressedData = try decompressGzip(data)
} else {
decompressedData = data
}
return try JSONDecoder().decode(T.self, from: decompressedData)
}
private func decompressGzip(_ data: Data) throws -> Data {
try (data as NSData).gunzipped() as Data
}
}
Compression Results
| Content Type | Original Size | Compressed | Reduction |
|---|---|---|---|
| JSON API Response | 100 KB | 25 KB | 75% |
| Transaction List (100) | 50 KB | 12 KB | 76% |
| Settings | 5 KB | 2 KB | 60% |
| ML Model Metadata | 10 KB | 3 KB | 70% |
Protocol Buffers
For high-frequency endpoints, Whistl uses Protocol Buffers:
Why Protocol Buffers?
- Smaller size: Binary format vs. text-based JSON
- Faster parsing: No string parsing required
- Strong typing: Schema-enforced types
- Backward compatible: Optional fields for versioning
Proto Definition
syntax = "proto3";
package whistl;
message Transaction {
string id = 1;
double amount = 2;
string merchant = 3;
string mcc = 4;
string category = 5;
int64 timestamp = 6;
optional double latitude = 7;
optional double longitude = 8;
optional double risk_score = 9;
}
message TransactionList {
repeated Transaction transactions = 1;
string cursor = 2;
bool has_more = 3;
}
message RiskAssessment {
double composite_score = 1;
RiskLevel level = 2;
repeated RiskFactor factors = 3;
int64 timestamp = 4;
}
enum RiskLevel {
RISK_LEVEL_UNSPECIFIED = 0;
RISK_LEVEL_LOW = 1;
RISK_LEVEL_ELEVATED = 2;
RISK_LEVEL_HIGH = 3;
RISK_LEVEL_CRITICAL = 4;
}
iOS Implementation
import SwiftProtobuf
class ProtobufClient {
func fetchTransactions(cursor: String?) async throws -> TransactionList {
var request = URLRequest(url: API.baseURL.appendingPathComponent("transactions"))
request.setValue("application/x-protobuf", forHTTPHeaderField: "Accept")
if let cursor = cursor {
request.url?.append(queryItems: [URLQueryItem(name: "cursor", value: cursor)])
}
let (data, _) = try await URLSession.shared.data(for: request)
return try Whistl_TransactionList(serializedData: data)
}
func sendRiskAssessment(_ assessment: RiskAssessment) async throws {
var request = URLRequest(url: API.baseURL.appendingPathComponent("risk"))
request.httpMethod = "POST"
request.setValue("application/x-protobuf", forHTTPHeaderField: "Content-Type")
request.httpBody = try assessment.serializedData()
try await URLSession.shared.data(for: request)
}
}
Size Comparison
| Format | Size (100 transactions) | Parse Time |
|---|---|---|
| JSON | 12 KB (gzipped) | 2.3ms |
| Protocol Buffers | 5.2 KB | 0.4ms |
| Savings | 57% | 83% faster |
Image Optimization
Goal images are optimised for mobile delivery:
Image Processing Pipeline
- Upload: Original image uploaded to server
- Processing: Server generates multiple sizes
- Format: WebP for Android, HEIC for iOS
- CDN: Images served from edge locations
Image Sizes
| Size | Dimensions | Use Case | Typical Size |
|---|---|---|---|
| Thumbnail | 100x100 | List views | 5 KB |
| Small | 300x300 | Card previews | 20 KB |
| Medium | 600x600 | Goal detail | 50 KB |
| Large | 1200x1200 | Full screen | 150 KB |
Responsive Image Loading
class ImageLoader {
func loadImage(for goal: Goal, size: ImageSize) async throws -> UIImage {
// Calculate appropriate size based on screen
let screenSize = UIScreen.main.bounds.size
let scale = UIScreen.main.scale
let targetSize = CGSize(
width: screenSize.width * scale,
height: screenSize.height * scale
)
// Request appropriately sized image
let url = goal.imageURL.resized(to: size, format: .webp)
// Check cache first
if let cached = ImageCache.shared.get(url) {
return cached
}
// Download
let (data, _) = try await URLSession.shared.data(from: url)
let image = try UIImage(webpData: data)
// Cache
ImageCache.shared.set(image, for: url)
return image
}
}
Delta Sync
Only changed data is synced, not full datasets:
Delta Sync Protocol
// Client sends last sync cursor
GET /transactions?since=cursor_abc123
// Server returns only changed transactions
{
"transactions": [
{"id": "txn-1", "amount": -50, "modified": true},
{"id": "txn-2", "deleted": true}
],
"new_cursor": "cursor_def456",
"has_more": false
}
// Client applies changes
// - Update txn-1
// - Remove txn-2
// - Save new cursor
Delta Sync Implementation
class DeltaSyncManager {
private var lastCursor: String?
func syncTransactions() async throws {
var cursor = lastCursor
var allChanges: [TransactionChange] = []
repeat {
let response = try await apiClient.getTransactionsDelta(since: cursor)
allChanges.append(contentsOf: response.changes)
cursor = response.newCursor
if !response.hasMore { break }
} while true
// Apply changes to local database
try localDB.applyChanges(allChanges)
// Save cursor
lastCursor = cursor
}
}
Bandwidth Savings
| Sync Type | Data Transferred | Savings |
|---|---|---|
| Full Sync | 500 KB | Baseline |
| Delta Sync (typical) | 15 KB | 97% |
| Delta Sync (no changes) | 0.5 KB | 99.9% |
Request Batching
Multiple requests are combined into single calls:
Batch Request Format
POST /batch
Content-Type: application/json
{
"requests": [
{
"id": "1",
"method": "GET",
"path": "/transactions?limit=10"
},
{
"id": "2",
"method": "GET",
"path": "/goals"
},
{
"id": "3",
"method": "GET",
"path": "/settings"
}
]
}
// Response
{
"responses": [
{"id": "1", "status": 200, "body": {...}},
{"id": "2", "status": 200, "body": {...}},
{"id": "3", "status": 200, "body": {...}}
]
}
Batch Benefits
- Fewer round trips: 1 request instead of 3+
- Reduced overhead: Single TCP/TLS handshake
- Better compression: Combined response compresses better
- Atomic operations: All-or-nothing execution
Bandwidth Monitoring
Users can track data usage in the app:
Usage Tracking
class BandwidthTracker {
private var cellularUsage: Int64 = 0
private var wifiUsage: Int64 = 0
func recordTransfer(_ bytes: Int64, overCellular: Bool) {
if overCellular {
cellularUsage += bytes
} else {
wifiUsage += bytes
}
}
func getMonthlyUsage() -> BandwidthReport {
return BandwidthReport(
cellular: cellularUsage,
wifi: wifiUsage,
total: cellularUsage + wifiUsage,
averagePerDay: (cellularUsage + wifiUsage) / 30
)
}
}
Typical Monthly Usage
| Activity | Monthly Data |
|---|---|
| Transaction Sync | 15 MB |
| Settings/Goals Sync | 2 MB |
| ML Model Updates | 2 MB |
| Blocklist Updates | 1 MB |
| Analytics (optional) | 5 MB |
| Total | 25 MB |
Data Saver Mode
Users on limited data can enable Data Saver:
Data Saver Features
- WiFi-only sync: No cellular data for background sync
- Reduced image quality: Lower resolution images
- Extended sync intervals: Less frequent updates
- Deferred model updates: Only on WiFi + charging
Data Saver Impact
| Metric | Normal | Data Saver | Reduction |
|---|---|---|---|
| Cellular Data/Month | 25 MB | 5 MB | 80% |
| Image Quality | 100% | 60% | — |
| Sync Frequency | Every 4h | Every 12h | 67% |
Conclusion
Whistl's compression strategies reduce bandwidth by 85% compared to uncompressed APIs. Through gzip compression, Protocol Buffers, image optimization, and delta sync, monthly data usage stays under 50MB—perfect for users with limited data plans.
Data Saver mode provides additional savings for users who need it, ensuring protection doesn't come at the cost of excessive data usage.
Get Efficient Protection
Whistl uses under 50MB of data per month with intelligent compression. Download free and experience bandwidth-efficient protection.
Download Whistl FreeRelated: Battery Optimization | Offline-First Architecture | Cloud Sync