diff --git a/src/lib/cloud-provider.js b/src/lib/cloud-provider.js
index 8116813ebd17c98549b3fc55565944747f19054d..1e6fb2adeca3635939aa9bf7c0e987bb2363ece2 100644
--- a/src/lib/cloud-provider.js
+++ b/src/lib/cloud-provider.js
@@ -16,24 +16,26 @@ class CloudProvider {
         this.vm = vm;
         this.username = username;
         this.projectId = projectId;
+        this.cloudHost = cloudHost;
 
-        // Open a websocket connection to the clouddata server
-        this.openConnection(cloudHost);
+        this.connectionAttempts = 0;
+
+        this.openConnection();
     }
 
     /**
      * Open a new websocket connection to the clouddata server.
      * @param {string} cloudHost The cloud data server to connect to.
      */
-    openConnection (cloudHost) {
-        if (window.WebSocket === null) {
-            log.warn('Websocket support is not available in this browser');
+    openConnection () {
+        try {
+            this.connection = new WebSocket((location.protocol === 'http:' ? 'ws://' : 'wss://') + this.cloudHost);
+        } catch (e) {
+            log.warn('Websocket support is not available in this browser', e);
             this.connection = null;
             return;
         }
 
-        this.connection = new WebSocket((location.protocol === 'http:' ? 'ws://' : 'wss://') + cloudHost);
-
         this.connection.onerror = this.onError.bind(this);
         this.connection.onmessage = this.onMessage.bind(this);
         this.connection.onopen = this.onOpen.bind(this);
@@ -42,8 +44,7 @@ class CloudProvider {
 
     onError (event) {
         log.error(`Websocket connection error: ${JSON.stringify(event)}`);
-        // TODO Add re-connection attempt logic here
-        this.clear();
+        // Error is always followed by close, which handles reconnect logic.
     }
 
     onMessage (event) {
@@ -57,12 +58,31 @@ class CloudProvider {
     }
 
     onOpen () {
+        this.connectionAttempts = 1; // Reset because we successfully connected
         this.writeToServer('handshake');
         log.info(`Successfully connected to clouddata server.`);
     }
 
     onClose () {
         log.info(`Closed connection to websocket`);
+        const exponentialTimeout = (Math.pow(2, this.connectionAttempts) - 1) * 1000;
+        const randomizedTimeout = this.randomizeDuration(exponentialTimeout);
+        this.setTimeout(this.reconnectNow.bind(this), randomizedTimeout);
+    }
+
+    reconnectNow () {
+        // Max connection attempts at 5, so timeout will max out in range [0, 31s]
+        this.connectionAttempts = Math.min(this.connectionAttempts + 1, 5);
+        this.openConnection();
+    }
+
+    randomizeDuration (t) {
+        return Math.random() * t;
+    }
+
+    setTimeout (fn, time) {
+        log.info(`Reconnecting in ${time}ms, attempt ${this.connectionAttempts}`);
+        this._connectionTimeout = window.setTimeout(fn, time);
     }
 
     parseMessage (message) {
@@ -148,7 +168,7 @@ class CloudProvider {
         if (this.connection &&
             this.connection.readyState !== WebSocket.CLOSING &&
             this.connection.readyState !== WebSocket.CLOSED) {
-
+            this.connection.onclose = () => {}; // Remove close listener to prevent reconnect
             this.connection.close();
         }
         this.clear();
@@ -163,6 +183,10 @@ class CloudProvider {
         this.vm = null;
         this.username = null;
         this.projectId = null;
+        if (this._connectionTimeout) {
+            clearTimeout(this._connectionTimeout);
+            this._connectionTimeout = null;
+        }
     }
 
 }
diff --git a/test/unit/util/cloud-provider.test.js b/test/unit/util/cloud-provider.test.js
index c81588ddeccad21e2e7e9116f9ae9d5ce7f081a2..d5f8a711222f7e3034a3069bca05a3a1fd47557f 100644
--- a/test/unit/util/cloud-provider.test.js
+++ b/test/unit/util/cloud-provider.test.js
@@ -1,33 +1,51 @@
 import CloudProvider from '../../../src/lib/cloud-provider';
 
-// Disable window.WebSocket
-global.WebSocket = null;
+let websocketConstructorCount = 0;
+
+// Stub the global websocket so we can call open/close/error/send on it
+global.WebSocket = function (url) {
+    this._url = url;
+    this._sentMessages = [];
+
+    // These are not real websocket methods, but used to trigger callbacks
+    this._open = () => this.onopen();
+    this._error = e => this.onerror(e);
+    this._receive = msg => this.onmessage(msg);
+
+    // Stub the real websocket.send to store sent messages
+    this.send = msg => this._sentMessages.push(msg);
+    this.close = () => this.onclose();
+
+    websocketConstructorCount++;
+};
+global.WebSocket.CLOSING = 'CLOSING';
+global.WebSocket.CLOSED = 'CLOSED';
 
 describe('CloudProvider', () => {
     let cloudProvider = null;
-    let sentMessage = null;
     let vmIOData = [];
-
+    let timeout = 0;
     beforeEach(() => {
         vmIOData = [];
         cloudProvider = new CloudProvider();
-        // Stub connection
-        cloudProvider.connection = {
-            send: msg => {
-                sentMessage = msg;
-            }
-        };
         // Stub vm
         cloudProvider.vm = {
             postIOData: (_namespace, data) => {
                 vmIOData.push(data);
             }
         };
+        // Stub setTimeout so this can run instantly.
+        cloudProvider.setTimeout = (fn, after) => {
+            timeout = after;
+            fn();
+        };
+        // Stub randomize to make it consistent for testing.
+        cloudProvider.randomizeDuration = t => t;
     });
 
     test('updateVariable', () => {
         cloudProvider.updateVariable('hello', 1);
-        const obj = JSON.parse(sentMessage);
+        const obj = JSON.parse(cloudProvider.connection._sentMessages[0]);
         expect(obj.method).toEqual('set');
         expect(obj.name).toEqual('hello');
         expect(obj.value).toEqual(1);
@@ -35,7 +53,7 @@ describe('CloudProvider', () => {
 
     test('updateVariable with falsey value', () => {
         cloudProvider.updateVariable('hello', 0);
-        const obj = JSON.parse(sentMessage);
+        const obj = JSON.parse(cloudProvider.connection._sentMessages[0]);
         expect(obj.method).toEqual('set');
         expect(obj.name).toEqual('hello');
         expect(obj.value).toEqual(0);
@@ -43,7 +61,7 @@ describe('CloudProvider', () => {
 
     test('writeToServer with falsey index value', () => {
         cloudProvider.writeToServer('method', 'name', 5, 0);
-        const obj = JSON.parse(sentMessage);
+        const obj = JSON.parse(cloudProvider.connection._sentMessages[0]);
         expect(obj.method).toEqual('method');
         expect(obj.name).toEqual('name');
         expect(obj.value).toEqual(5);
@@ -55,7 +73,7 @@ describe('CloudProvider', () => {
             method: 'ack',
             name: 'name'
         });
-        cloudProvider.onMessage({data: msg});
+        cloudProvider.connection._receive({data: msg});
         expect(vmIOData[0].varCreate.name).toEqual('name');
     });
 
@@ -65,7 +83,7 @@ describe('CloudProvider', () => {
             name: 'name',
             value: 'value'
         });
-        cloudProvider.onMessage({data: msg});
+        cloudProvider.connection._receive({data: msg});
         expect(vmIOData[0].varUpdate.name).toEqual('name');
         expect(vmIOData[0].varUpdate.value).toEqual('value');
     });
@@ -80,8 +98,60 @@ describe('CloudProvider', () => {
             method: 'ack',
             name: 'name2'
         });
-        cloudProvider.onMessage({data: `${msg1}\n${msg2}`});
+        cloudProvider.connection._receive({data: `${msg1}\n${msg2}`});
         expect(vmIOData[0].varUpdate.name).toEqual('name1');
         expect(vmIOData[1].varCreate.name).toEqual('name2');
     });
+
+    test('connecting sets connnection attempts back to 1', () => {
+        expect(cloudProvider.connectionAttempts).toBe(0);
+        cloudProvider.connectionAttempts = 10;
+        cloudProvider.connection._open();
+        expect(cloudProvider.connectionAttempts).toBe(1);
+    });
+
+    test('disconnect waits for a period equal to 2^k-1 before trying again', () => {
+        websocketConstructorCount = 1; // This is global, so set it back to 1 to start
+
+        // Connection attempts should still be 0 because connection hasn't opened yet
+        expect(cloudProvider.connectionAttempts).toBe(0);
+        cloudProvider.connection._open();
+        expect(cloudProvider.connectionAttempts).toBe(1);
+
+        cloudProvider.connection.close();
+        expect(timeout).toEqual(1 * 1000); // 2^1 - 1
+        expect(websocketConstructorCount).toBe(2);
+        expect(cloudProvider.connectionAttempts).toBe(2);
+
+        cloudProvider.connection.close();
+        expect(timeout).toEqual(3 * 1000); // 2^2 - 1
+        expect(websocketConstructorCount).toBe(3);
+        expect(cloudProvider.connectionAttempts).toBe(3);
+
+        cloudProvider.connection.close();
+        expect(timeout).toEqual(7 * 1000); // 2^3 - 1
+        expect(websocketConstructorCount).toBe(4);
+        expect(cloudProvider.connectionAttempts).toBe(4);
+
+        cloudProvider.connection.close();
+        expect(timeout).toEqual(15 * 1000); // 2^4 - 1
+        expect(websocketConstructorCount).toBe(5);
+        expect(cloudProvider.connectionAttempts).toBe(5);
+
+        cloudProvider.connection.close();
+        expect(timeout).toEqual(31 * 1000); // 2^5 - 1
+        expect(websocketConstructorCount).toBe(6);
+        expect(cloudProvider.connectionAttempts).toBe(5); // Maxed at 5
+
+        cloudProvider.connection.close();
+        expect(timeout).toEqual(31 * 1000); // maxed out at 2^5 - 1
+        expect(websocketConstructorCount).toBe(7);
+        expect(cloudProvider.connectionAttempts).toBe(5); // Maxed at 5
+    });
+
+    test('requestCloseConnection does not try to reconnect', () => {
+        websocketConstructorCount = 1; // This is global, so set it back to 1 to start
+        cloudProvider.requestCloseConnection();
+        expect(websocketConstructorCount).toBe(1); // No reconnection attempts
+    });
 });