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 TypeOriginal SizeCompressedReduction
JSON API Response100 KB25 KB75%
Transaction List (100)50 KB12 KB76%
Settings5 KB2 KB60%
ML Model Metadata10 KB3 KB70%

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

FormatSize (100 transactions)Parse Time
JSON12 KB (gzipped)2.3ms
Protocol Buffers5.2 KB0.4ms
Savings57%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

SizeDimensionsUse CaseTypical Size
Thumbnail100x100List views5 KB
Small300x300Card previews20 KB
Medium600x600Goal detail50 KB
Large1200x1200Full screen150 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 TypeData TransferredSavings
Full Sync500 KBBaseline
Delta Sync (typical)15 KB97%
Delta Sync (no changes)0.5 KB99.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

ActivityMonthly Data
Transaction Sync15 MB
Settings/Goals Sync2 MB
ML Model Updates2 MB
Blocklist Updates1 MB
Analytics (optional)5 MB
Total25 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

MetricNormalData SaverReduction
Cellular Data/Month25 MB5 MB80%
Image Quality100%60%
Sync FrequencyEvery 4hEvery 12h67%

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 Free

Related: Battery Optimization | Offline-First Architecture | Cloud Sync