diff --git a/config/config.sample.php b/config/config.sample.php
index 01abc583688f31845197aed59c51be6c0a56b085..ef5fb7ea5a5f685fa2a60e857d9738e9c37ad848 100755
--- a/config/config.sample.php
+++ b/config/config.sample.php
@@ -184,6 +184,13 @@ $CONFIG = array(
 /* Life time of a session after inactivity */
 "session_lifetime" => 60 * 60 * 24,
 
+/*
+ * Enable/disable session keep alive when a user is logged in in the Web UI.
+ * This is achieved by sending a "heartbeat" to the server to prevent
+ * the session timing out.
+ */
+"session_keepalive" => true,
+
 /* Custom CSP policy, changing this will overwrite the standard policy */
 "custom_csp_policy" => "default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; frame-src *; img-src *; font-src 'self' data:; media-src *",
 
diff --git a/core/js/config.php b/core/js/config.php
index dd46f7889d155ab80ff00fef7fcc892da516070e..517ea1615a8fdf4187cf98f3244fc319ce6d6caf 100644
--- a/core/js/config.php
+++ b/core/js/config.php
@@ -55,6 +55,12 @@ $array = array(
 		)
 	),
 	"firstDay" => json_encode($l->l('firstday', 'firstday')) ,
+	"oc_config" => json_encode(
+		array(
+			'session_lifetime' => \OCP\Config::getSystemValue('session_lifetime', 60 * 60 * 24),
+			'session_keepalive' => \OCP\Config::getSystemValue('session_keepalive', true)
+		)
+	)
 	);
 
 // Echo it
diff --git a/core/js/js.js b/core/js/js.js
index 1c7d89ea055adbb0344c596111c67daca0f62102..cb177712a3a9ad46532453a65941f6fb028c875e 100644
--- a/core/js/js.js
+++ b/core/js/js.js
@@ -11,6 +11,8 @@ var oc_webroot;
 var oc_current_user = document.getElementsByTagName('head')[0].getAttribute('data-user');
 var oc_requesttoken = document.getElementsByTagName('head')[0].getAttribute('data-requesttoken');
 
+window.oc_config = window.oc_config || {};
+
 if (typeof oc_webroot === "undefined") {
 	oc_webroot = location.pathname;
 	var pos = oc_webroot.indexOf('/index.php/');
@@ -742,8 +744,39 @@ function fillWindow(selector) {
 	console.warn("This function is deprecated! Use CSS instead");
 }
 
-$(document).ready(function(){
-	sessionHeartBeat();
+/**
+ * Initializes core
+ */
+function initCore() {
+
+	/**
+	 * Calls the server periodically to ensure that session doesn't
+	 * time out
+	 */
+	function initSessionHeartBeat(){
+		// interval in seconds
+		var interval = 900;
+		if (oc_config.session_lifetime) {
+			interval = Math.floor(oc_config.session_lifetime / 2);
+		}
+		// minimum one minute
+		if (interval < 60) {
+			interval = 60;
+		}
+		OC.Router.registerLoadedCallback(function(){
+			var url = OC.Router.generate('heartbeat');
+			setInterval(function(){
+				$.post(url);
+			}, interval * 1000);
+		});
+	}
+
+	// session heartbeat (defaults to enabled)
+	if (typeof(oc_config.session_keepalive) === 'undefined' ||
+		!!oc_config.session_keepalive) {
+
+		initSessionHeartBeat();
+	}
 
 	if(!SVGSupport()){ //replace all svg images with png images for browser that dont support svg
 		replaceSVG();
@@ -856,7 +889,9 @@ $(document).ready(function(){
 	$('input[type=text]').focus(function(){
 		this.select();
 	});
-});
+}
+
+$(document).ready(initCore);
 
 /**
  * Filter Jquery selector by attribute value
@@ -986,15 +1021,3 @@ jQuery.fn.exists = function(){
 	return this.length > 0;
 };
 
-/**
- * Calls the server periodically every 15 mins to ensure that session doesnt
- * time out
- */
-function sessionHeartBeat(){
-	OC.Router.registerLoadedCallback(function(){
-		var url = OC.Router.generate('heartbeat');
-		setInterval(function(){
-			$.post(url);
-		}, 900000);
-	});
-}
diff --git a/core/js/tests/specHelper.js b/core/js/tests/specHelper.js
index 4a30878df514b8b559baa246688997ad2a98a60d..1848d08354e02c7c500dbd93e668f39aa03ef61c 100644
--- a/core/js/tests/specHelper.js
+++ b/core/js/tests/specHelper.js
@@ -19,6 +19,8 @@
 *
 */
 
+/* global OC */
+
 /**
  * Simulate the variables that are normally set by PHP code
  */
@@ -57,10 +59,15 @@ window.oc_webroot = location.href + '/';
 window.oc_appswebroots = {
 	"files": window.oc_webroot + '/apps/files/'
 };
+window.oc_config = {
+	session_lifetime: 600 * 1000,
+	session_keepalive: false
+};
 
 // global setup for all tests
 (function setupTests() {
-	var fakeServer = null;
+	var fakeServer = null,
+		routesRequestStub;
 
 	beforeEach(function() {
 		// enforce fake XHR, tests should not depend on the server and
@@ -78,9 +85,18 @@ window.oc_appswebroots = {
 		// make it globally available, so that other tests can define
 		// custom responses
 		window.fakeServer = fakeServer;
+
+		OC.Router.routes = [];
+		OC.Router.routes_request = {
+			state: sinon.stub().returns('resolved'),
+			done: sinon.stub()
+		};
 	});
 
 	afterEach(function() {
+		OC.Router.routes_request.state.reset();
+		OC.Router.routes_request.done.reset();
+
 		// uncomment this to log requests
 		// console.log(window.fakeServer.requests);
 		fakeServer.restore();
diff --git a/core/js/tests/specs/coreSpec.js b/core/js/tests/specs/coreSpec.js
index 85fae7567b313dd302843faa39b6395bc2e44d3c..478505e928767fb7fd2aa190a76451b8b7e54848 100644
--- a/core/js/tests/specs/coreSpec.js
+++ b/core/js/tests/specs/coreSpec.js
@@ -203,4 +203,78 @@ describe('Core base tests', function() {
 			})).toEqual('number=123');
 		});
 	});
+	describe('Session heartbeat', function() {
+		var clock,
+			oldConfig,
+			loadedStub,
+			routeStub,
+			counter;
+
+		beforeEach(function() {
+			clock = sinon.useFakeTimers();
+			oldConfig = window.oc_config;
+			loadedStub = sinon.stub(OC.Router, 'registerLoadedCallback');
+			routeStub = sinon.stub(OC.Router, 'generate').returns('/heartbeat');
+			counter = 0;
+
+			fakeServer.autoRespond = true;
+			fakeServer.autoRespondAfter = 0;
+			fakeServer.respondWith(/\/heartbeat/, function(xhr) {
+				counter++;
+				xhr.respond(200, {'Content-Type': 'application/json'}, '{}');
+			});
+		});
+		afterEach(function() {
+			clock.restore();
+			window.oc_config = oldConfig;
+			loadedStub.restore();
+			routeStub.restore();
+		});
+		it('sends heartbeat half the session lifetime when heartbeat enabled', function() {
+			window.oc_config = {
+				session_keepalive: true,
+				session_lifetime: 300
+			};
+			window.initCore();
+			expect(loadedStub.calledOnce).toEqual(true);
+			loadedStub.yield();
+			expect(routeStub.calledWith('heartbeat')).toEqual(true);
+
+			expect(counter).toEqual(0);
+
+			// less than half, still nothing
+			clock.tick(100 * 1000);
+			expect(counter).toEqual(0);
+
+			// reach past half (160), one call
+			clock.tick(55 * 1000);
+			expect(counter).toEqual(1);
+
+			// almost there to the next, still one
+			clock.tick(140 * 1000);
+			expect(counter).toEqual(1);
+
+			// past it, second call
+			clock.tick(20 * 1000);
+			expect(counter).toEqual(2);
+		});
+		it('does no send heartbeat when heartbeat disabled', function() {
+			window.oc_config = {
+				session_keepalive: false,
+				session_lifetime: 300
+			};
+			window.initCore();
+			expect(loadedStub.notCalled).toEqual(true);
+			expect(routeStub.notCalled).toEqual(true);
+
+			expect(counter).toEqual(0);
+
+			clock.tick(1000000);
+
+			// still nothing
+			expect(counter).toEqual(0);
+		});
+
+	});
 });
+