🏖️ Get once, always work, update if possible : Kahiether network strategies 🌐
Hello,
Pre requirement : This strategy is very related to the microservice pattern where all the logic is stored fully in client side. We need to remember that this pattern doesn't works for use cases where some logic and data are manage by the distant server.
This time, we'll explore how to build web applications and mobile / desktop PWA taking account of network constraints - from fiber-optic highways to the most challenging network environments on Earth. This story started 20 years ago in Madagascar.
Network Reality Check
When 56k Was a Dream
Picture this: Madagascar, 2005 - 2010. While the world was transitioning from dial-up to broadband, we were celebrating when our connection stayed stable long enough to download a single MP3 file. I personally remember spending 2-3 days downloading just a few megabytes, forcing to switch from direct download to using eMule and BitTorrent's partial download features to grab chunks.
The strategy was simple but effective:
- Download small chunks when possible
- Resume from where we left off
- Pray that some parts of the file is not missing
- The file will be usable only if the full chunks are downloaded
Sound primitive? Maybe. But it taught me invaluable lessons about resilience and the importance of designing for the worst-case scenario.
Fast Forward to Today: Not Much Has Changed
We would think that 20 years later, with 4G, 5G, satellite network and fiber optics spreading globally, these problems would be ancient history. But, some of the challenges are still there:
- Connection speeds might be better on paper, but reliability remains abysmal especially when moving out in the deep countryside
- Power outages are still frequent and long
- Network interruptions happen without warning
- Data costs make every megabyte precious
The infrastructure has improved, but the fundamental challenges persist. This reality shaped my approach to building solutions that actually work for everyone, not just those with gigabit connections.
The Problem: Traditional Caching Strategies Fail
Why Standard Approaches Don't Work
Most web caching strategies assume:
- Stable connections (even if slow)
- Ability to complete HTTP requests
- Predictable timeout behaviors
- Power reliability
- Stable servers (no attacks, no errors)
In Madagascar-like conditions, these assumptions crumble:
When your connection drops every few minutes and power cuts mid-download, traditional cache-first or network-first strategies leave users stranded.
The Solution: Adaptive Resilience Strategy
Learning from P2P: The Chunking Philosophy
Remember how eMule and BitTorrent solved the download problem? They broke files into chunks, grabbed what they could, and assembled the pieces later. We can apply similar thinking by considering each file as chunk to web apps, and add a microservice pattern to reduce the number of chunks managed in a single app :
javascript
const CONFIG = {
CACHE_NAME: self.SW_CACHE_NAME || 'faritany-v2',
TEMP_CACHE_NAME: self.SW_TEMP_CACHE_NAME || 'faritany-temp-v2',
FIRST_TIME_TIMEOUT: parseInt(self.SW_FIRST_TIME_TIMEOUT) || 30000, // 30 seconds
RETURNING_USER_TIMEOUT: parseInt(self.SW_RETURNING_USER_TIMEOUT) || 5000, // 5 seconds
};
The Strategy
After a few years of refinement, here's the last approach that has proven successful since now :
1. First Contact: Maximum Patience
New users need to download the app at least once. We give them every chance and time to fetch all the assets:
async function fetchFromNetworkWithExtendedTimeout(request) {
try {
console.log(`Service Worker: First time user - extended network timeout: ${request.url}`);
// Extended timeout for first-time users (30 seconds)
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Network request timeout - first time user')), CONFIG.FIRST_TIME_TIMEOUT);
});
const networkResponse = await Promise.race([
fetch(request),
timeoutPromise
]);
if (!networkResponse.ok) {
throw new Error(`Server error: ${networkResponse.status}`);
}
// SUCCESS: Cache for future use
console.log(`Service Worker: First time success - caching: ${request.url}`);
const cache = await caches.open(LIVE_CACHE);
cache.put(request, networkResponse.clone());
return networkResponse;
This mirrors the patience we had downloading files 20 years ago - sometimes you just need to wait for that one good moment.
2. Returning Users: Try network of fallbacks
When a user returns, he already have a working version on his cache, so if he has network we try to update, but if the update takes too much time to complete, then we cut the update and fallback on cache:
javascript
// Create timeout promise (5 seconds max wait)
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Network request timeout')), CONFIG.RETURNING_USER_TIMEOUT);
});
3. Power Outage Protection: Atomic Updates
Nothing's worse than a half-updated app after power returns. So we need to check that everything is updated before switching the existing version:
javascript
// Install: Download all assets into a temporary cache.
self.addEventListener('install', event => {
console.log('Service Worker: Installing...');
self.skipWaiting(); // Force immediate activation
event.waitUntil(
caches.open(TEMP_CACHE).then(tempCache => {
return Promise.all(
ASSETS.map(url => {
return fetch(url).then(response => {
if (!response.ok) {
throw new Error(`Failed to fetch ${url}: ${response.status}`);
}
console.log(`Service Worker: Cached ${url}`);
return tempCache.put(url, response.clone());
}).catch(error => {
console.error(`Service Worker: Failed to cache ${url}:`, error);
// Continue with other assets even if one fails
return null;
});
})
);
})
);
});
// Activate: Replace live cache ONLY if ALL assets are staged
self.addEventListener('activate', event => {
console.log('Service Worker: Activating...');
event.waitUntil(
(async () => {
const tempCache = await caches.open(TEMP_CACHE);
const cachedRequests = await tempCache.keys();
// ALL ASSETS ARE CRITICAL - Strict verification
if (cachedRequests.length === ASSETS.length) {
console.log('Service Worker: ALL assets staged successfully, updating live cache');
// Complete atomic replacement
await caches.delete(LIVE_CACHE);
const liveCache = await caches.open(LIVE_CACHE);
// Copy ALL assets from temp cache to live cache
for (const request of cachedRequests) {
const response = await tempCache.match(request);
await liveCache.put(request, response);
}
Real-World Implementation Details
Cache lock rescue
At the time of this update some code was already running with a specific version, so we need to implement the rescue process. The main issue that must be checked is the cache lock, so the solution is to intercept the main.js and inject inside of it the rescue code.
// server.js
const express = require('express');
const path = require('path');
const fs = require('fs');
const app = express();
// CACHE VERSION MANAGEMENT - Change this to deploy new version
const CACHE_VERSION = process.env.CACHE_VERSION || 'v2';
const APP_NAME = process.env.APP_NAME || 'faritany';
// Cache Lock Rescue - Intercept main.js to inject rescue code
app.get('/main.js', (req, res) => {
try {
// Read the actual main.js file (your existing game/app code)
let jsContent = fs.readFileSync(path.join(__dirname, 'main.js'), 'utf8');
// Inject ONLY the rescue detection code at the beginning
const rescueCode = `
// Cache Lock Rescue - Check for ${CACHE_VERSION} users and free older versions
if ('serviceWorker' in navigator) {
caches.keys().then(cacheNames => {
const hasCurrentVersion = cacheNames.some(name => name.includes('-${CACHE_VERSION}'));
if (!hasCurrentVersion && cacheNames.length > 0) {
// Old version detected - unregister and reload
console.log('Cache lock detected - rescuing to ${CACHE_VERSION}...');
navigator.serviceWorker.getRegistration().then(reg => {
if (reg) reg.unregister().then(() => location.reload());
});
return; // Stop here for old version users
}
// Current version users or new users - normal service worker registration
navigator.serviceWorker.register('/service-worker.js', {updateViaCache: 'none'});
});
}
`;
Lessons from the Field
What Really Matters
- Assume Nothing: That 4G/5G signal might drop to EDGE without warning or come back to normal without warning
- Cache Everything: Every byte downloaded is precious
- Fail Gracefully: Errors should guide, not frustrate
- Background Updates: Never completely block the user experience
- The user usually manages to cut itself his data when it is not unlimited: Most user switch off data and uses specific prices for Facebook mobile only and Whatapp mobile only
The Bigger Picture
Why This Matters
Building for challenging situations is inside Kahiether's DNA.
- Performance Benefits Everyone: Optimizations for slow networks make fast networks blazing, because it's a network first strategy, those with very good internet speed will not notice anything different.
- Resilience is Universal: Power outages might happen in Silicon Valley too.
- Future-Proofing: Today's edge case is tomorrow's standard requirement. We don't know maybe tomorrow we will use these apps in the moon, in the space, in Mars, under the Ocean 's Abysses
- Global Market Access: Billions of users are in emerging markets
Beyond Madagascar
There is so much devices in the world, I have particularly noticed users that have more than one smartphone when double SIM wasn't that popular. Some of them uses a phone with an Orange SIM because they give unlimited Facebook for a day for a certain amount, and another phone with a Telma (Yas) SIM because the network is cheaper for every Mb consumed. But these patterns apply everywhere in a lot of different situations:
- Rural Areas: Spotty coverage in developed countries
- Transportation: Subways, flights, tunnels
- Emergencies: When infrastructure fails
- Cost-Conscious Users: Those managing data budgets
Wrapping Up
Twenty years after struggling to download files in Madagascar, the core challenges remain remarkably similar. But now we have better tools and strategies to handle them. The key insights:
Building truly resilient web applications requires embracing the reality that good networks are the exception, not the rule. Whether it's a specific country challenging infrastructure or a subway tunnel in New York, our apps should work everywhere, for everyone.
This strategy consists on three main steps :
Get once : get the app once
Always works : cache everything for work offline
Update if possible : update only if you are sure to be able to make a full update
This is not the perfect solution, but it is what it is. Switching frow the two network environment really taught me one lesson :
Some innovations emerge from the toughest conditions. Progress begins when open tools empower everyone, everywhere.
See you next time! 🌍