diff --git a/server/app.py b/server/app.py
index 88ee923a826c08c676751a43a1604e3713c2705b..84f691c773c28b5c497bf86ffafe159298622575 100644
--- a/server/app.py
+++ b/server/app.py
@@ -70,7 +70,8 @@ def getViewData(request, stopId, view):
 		return {
 			'arrivals': gtfs.getNextTrips(stopId, limit = 2, routeType = GtfsConstants.ROUTE_HEAD),
 			'gmaps_key': GOOGLE_MAPS_KEY if not app.config['DEBUG'] else GOOGLE_MAPS_TEST_KEY,
-			'gmaps_region': GOOGLE_MAPS_REGION 
+			'gmaps_region': GOOGLE_MAPS_REGION,
+			'service_status': { 'active': not locator.isStale() }
 		}
 
 	return {}
@@ -307,6 +308,8 @@ def apiLocatorService(version, type = 'routed'):
 		lastModified = data = None
 		if g.stop_id == 'raw' or not g.stop_id:
 			lastModified, data = locator.getLastResponse()
+		elif g.stop_id == 'status':
+			return jsonify({ 'active': not locator.isStale() })
 		elif type == 'routed':
 			lastModified, data = gtfs.locateRoutedVehicles(g.stop_id)
 		elif type == 'nearby':
@@ -346,7 +349,10 @@ def apiBusInfo(version, busId = None, getShape = None):
 			data["shapes"] = gtfs.getTripShapes(data.get('lineref', None), data.get('blockref', None))
 
 		if data:
-			return jsonify(data)
+			response = jsonify(data)
+			response.last_modified = lastModified
+			response.expires = lastModified + timedelta(seconds = 2)
+			return response.make_conditional(request)
 
 		abort(404)
 
diff --git a/server/locator.py b/server/locator.py
index e69b31513b47d643298026008c45abe89f7c11eb..b3caf92f299395e9c044fba34b3f8325478d4d57 100644
--- a/server/locator.py
+++ b/server/locator.py
@@ -1,7 +1,9 @@
 # coding=utf-8
 
 import requests, threading, time, json, six, math
+
 from datetime import datetime
+from requests.exceptions import ConnectionError, HTTPError, Timeout, RequestException
 
 # This really should be written using multiprocessing instead,
 # because of the GIL, but setting up full 2-way communication 
@@ -18,6 +20,7 @@ class PollingThreadReader(object):
 	_running = False
 	_response = None
 	_modified = datetime.utcfromtimestamp(0)
+	_stale = False
 
 	def __init__(self, endpointURI, userAgent, pollFreq = 3):
 		self.endpoint = endpointURI
@@ -52,16 +55,33 @@ class PollingThreadReader(object):
 
 	def run(self):
 		while self._running:
-			response = requests.get(self.endpoint, headers = self.requestHeaders).content
+			apiResponse = None
+
+			# FIXME: better exception handling
+			try:
+				apiResponse = requests.get(self.endpoint, headers = self.requestHeaders).content
+			except (ConnectionError, HTTPError, Timeout):
+				self._stale = True
+				time.sleep(self.pollTime)
+				continue
+			except RequestException:
+				self._stale = True
+				continue
+
 			with self._lock:
 				try:
-					self._response = json.loads(response)
+					self._response = json.loads(apiResponse)
 					self._modified = datetime.utcnow()
+					self._stale = False
 				except ValueError:
 					pass
 
 			time.sleep(self.pollTime)
 
+	def isStale(self):
+		# not thread safe, only usable as a suggestion that the response may be stale
+		return self._stale
+
 	def getLastResponse(self):
 		with self._lock:
 			return (self._modified, self._response)
diff --git a/templates/map.html b/templates/map.html
index c7c2e9383daf1d011c7ab5cc25035d90a8a1759d..6e2f3cbc3ee4736cc0492df3bbaa3d714fb5955e 100644
--- a/templates/map.html
+++ b/templates/map.html
@@ -50,6 +50,15 @@
 		</div>
 	</div>
 
+	{% if not service_status.active %}
+		<div class="alert alert-info alert-dismissible box-shadow text-center show" role="alert">
+			{{ _("The location service is currently not active, the map may show outdated information.") }}
+			<!-- <button type="button" class="close" data-dismiss="alert" aria-label="Close">
+				<span aria-hidden="true">&times;</span>
+			</button> -->
+		</div>
+	{% endif %}
+
 	<!-- draw the map -->
 	<div class="card border-foli box-shadow">
 		<div class="card-body p-0">
@@ -268,7 +277,9 @@
 
 					busTrackingLoop("all");
 				}
-			);
+			).fail(function() {
+				console.log('unable to start tracking vehicles, service unavailable');
+			});
 		};
 
 		var drawAllBuses = function() {
@@ -278,8 +289,10 @@
 			$.getJSON("{{ url_for('apiLocatorService', version = 1, stopId = stop_info.stop_id, type = 'nearby', _external = True) }}",
 				function (data) {
 					// poor man's locking
-					if (trackingLoop === null)
+					if (trackingLoop === null) {
+						completeMapUpdate();
 						return;
+					}
 
 					// temporarily move all markers to a temp list
 					// one by one all active markers will be moved back to the markers list
@@ -310,7 +323,9 @@
 
 					completeMapUpdate();
 				}
-			)
+			).fail(function() {
+				completeMapUpdate();
+			});
 		};
 
 		var initOneBusTracking = function() {
@@ -339,8 +354,9 @@
 				{ bus_id: requestFocus, get_shape: 1 },
 				function (data) {
 					// poor man's locking (the A in Ajax is for Asynchronous :))
-					if (requestFocus !== focusedMarker)
+					if (requestFocus !== focusedMarker) {
 						return;
+					}
 
 					focusedBus = data;
 					console.log("found this bus info: ", focusedBus);
@@ -364,18 +380,21 @@
 					});
 
 					updateInfoWindow(true);
-				},
-				'json'
-			);
 
-			marker = gmarkers[focusedMarker];
-			gmarkers = {};
-			gmarkers[focusedMarker] = marker;
+					marker = gmarkers[focusedMarker];
+					gmarkers = {};
+					gmarkers[focusedMarker] = marker;
 
-			gmap.panTo(marker.getPosition());
-			// gmap.setZoom(mapZoom + 1); Föli provided shapes do not line up well enough with Google map tiles to make this a good experience if zoomed
+					gmap.panTo(marker.getPosition());
+					// gmap.setZoom(mapZoom + 1); Föli provided shapes do not line up well enough with Google map tiles to make this a good experience if zoomed
 
-			busTrackingLoop("one");
+					busTrackingLoop("one");
+				},
+				'json'
+			).fail(function() {
+				console.log('lost track of the bus, going back to normal mode');
+				mapClicked();
+			});
 		};
 
 		var drawOneBus = function() {
@@ -388,8 +407,10 @@
 				{ bus_id: requestFocus, get_shape: 0 },
 				function (data) {
 					// poor man's locking
-					if (trackingLoop === null || requestFocus !== focusedMarker)
+					if (trackingLoop === null || requestFocus !== focusedMarker) {
+						completeMapUpdate();
 						return;
+					}
 
 					// if the focused bus disappeared from the list, just go back to the 'all' mode
 					if ($.isEmptyObject(data) || gmarkers[focusedMarker] === undefined) {
@@ -411,7 +432,12 @@
 					completeMapUpdate();
 				},
 				'json'
-			)
+			).fail(function() {
+				console.log('lost track of the bus, going back to normal mode');
+				mapClicked();
+
+				completeMapUpdate();
+			});
 		};
 
 		// creates or updates the info window for focused bus