diff --git a/src/lib/project-saver-hoc.jsx b/src/lib/project-saver-hoc.jsx
index 2487531bb65275fa62146e5dd0664e2a02f3e15a..bbd78f4463919a920cb9cdd822b3b8ad8a9a87c5 100644
--- a/src/lib/project-saver-hoc.jsx
+++ b/src/lib/project-saver-hoc.jsx
@@ -1,3 +1,4 @@
+import bindAll from 'lodash.bindall';
 import React from 'react';
 import PropTypes from 'prop-types';
 import {connect} from 'react-redux';
@@ -9,6 +10,7 @@ import {
     showAlertWithTimeout,
     showStandardAlert
 } from '../reducers/alerts';
+import {setAutoSaveTimeoutId} from '../reducers/timeout';
 import {setProjectUnchanged} from '../reducers/project-changed';
 import {
     LoadingStates,
@@ -37,7 +39,16 @@ import {
  */
 const ProjectSaverHOC = function (WrappedComponent) {
     class ProjectSaverComponent extends React.Component {
+        constructor (props) {
+            super(props);
+            bindAll(this, [
+                'tryToAutoSave'
+            ]);
+        }
         componentDidUpdate (prevProps) {
+            if (this.props.projectChanged && !prevProps.projectChanged) {
+                this.scheduleAutoSave();
+            }
             if (this.props.isUpdating && !prevProps.isUpdating) {
                 this.updateProjectToStorage();
             }
@@ -65,10 +76,30 @@ const ProjectSaverHOC = function (WrappedComponent) {
             // don't try to save immediately after trying to save
             if (prevProps.isUpdating) return;
             // if we're newly able to save this project, save it!
-            const showingSaveable = this.props.canSave && this.props.isShowingWithId;
             const becameAbleToSave = this.props.canSave && !prevProps.canSave;
             const becameShared = this.props.isShared && !prevProps.isShared;
-            if (showingSaveable && (becameAbleToSave || becameShared)) {
+            if (this.props.isShowingSaveable && (becameAbleToSave || becameShared)) {
+                this.props.onAutoUpdateProject();
+            }
+        }
+        componentWillUnmount () {
+            this.clearAutoSaveTimeout();
+        }
+        clearAutoSaveTimeout () {
+            if (this.props.autoSaveTimeoutId !== null) {
+                clearTimeout(this.props.autoSaveTimeoutId);
+                this.props.setAutoSaveTimeoutId(null);
+            }
+        }
+        scheduleAutoSave () {
+            if (this.props.isShowingSaveable && this.props.autoSaveTimeoutId === null) {
+                const timeoutId = setTimeout(this.tryToAutoSave,
+                    this.props.autosaveIntervalSecs * 1000);
+                this.props.setAutoSaveTimeoutId(timeoutId);
+            }
+        }
+        tryToAutoSave () {
+            if (this.props.projectChanged && this.props.isShowingSaveable) {
                 this.props.onAutoUpdateProject();
             }
         }
@@ -143,6 +174,7 @@ const ProjectSaverHOC = function (WrappedComponent) {
          */
         storeProject (projectId, requestParams) {
             requestParams = requestParams || {};
+            this.clearAutoSaveTimeout();
             return Promise.all(this.props.vm.assets
                 .filter(asset => !asset.clean)
                 .map(
@@ -180,10 +212,13 @@ const ProjectSaverHOC = function (WrappedComponent) {
         render () {
             const {
                 /* eslint-disable no-unused-vars */
+                autosaveIntervalSecs,
                 isCreatingCopy,
                 isCreatingNew,
+                projectChanged,
                 isManualUpdating,
                 isRemixing,
+                isShowingSaveable,
                 isShowingWithId,
                 isShowingWithoutId,
                 isUpdating,
@@ -200,6 +235,7 @@ const ProjectSaverHOC = function (WrappedComponent) {
                 onUpdatedProject,
                 reduxProjectId,
                 reduxProjectTitle,
+                setAutoSaveTimeoutId: setAutoSaveTimeoutIdProp,
                 /* eslint-enable no-unused-vars */
                 ...componentProps
             } = this.props;
@@ -212,6 +248,7 @@ const ProjectSaverHOC = function (WrappedComponent) {
     }
 
     ProjectSaverComponent.propTypes = {
+        autoSaveTimeoutId: PropTypes.number,
         canCreateNew: PropTypes.bool,
         canSave: PropTypes.bool,
         isCreatingCopy: PropTypes.bool,
@@ -219,6 +256,7 @@ const ProjectSaverHOC = function (WrappedComponent) {
         isManualUpdating: PropTypes.bool,
         isRemixing: PropTypes.bool,
         isShared: PropTypes.bool,
+        isShowingSaveable: PropTypes.bool,
         isShowingWithId: PropTypes.bool,
         isShowingWithoutId: PropTypes.bool,
         isUpdating: PropTypes.bool,
@@ -233,21 +271,29 @@ const ProjectSaverHOC = function (WrappedComponent) {
         onShowSaveSuccessAlert: PropTypes.func,
         onShowSavingAlert: PropTypes.func,
         onUpdatedProject: PropTypes.func,
+        projectChanged: PropTypes.bool,
         reduxProjectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
         reduxProjectTitle: PropTypes.string,
         vm: PropTypes.instanceOf(VM).isRequired
     };
-    const mapStateToProps = state => {
+    ProjectSaverComponent.defaultProps = {
+        autosaveIntervalSecs: 120
+    };
+    const mapStateToProps = (state, ownProps) => {
         const loadingState = state.scratchGui.projectState.loadingState;
+        const isShowingWithId = getIsShowingWithId(loadingState);
         return {
+            autoSaveTimeoutId: state.scratchGui.timeout.autoSaveTimeoutId,
             isCreatingCopy: getIsCreatingCopy(loadingState),
             isCreatingNew: getIsCreatingNew(loadingState),
             isRemixing: getIsRemixing(loadingState),
-            isShowingWithId: getIsShowingWithId(loadingState),
+            isShowingSaveable: ownProps.canSave && isShowingWithId,
+            isShowingWithId: isShowingWithId,
             isShowingWithoutId: getIsShowingWithoutId(loadingState),
             isUpdating: getIsUpdating(loadingState),
             isManualUpdating: getIsManualUpdating(loadingState),
             loadingState: loadingState,
+            projectChanged: state.scratchGui.projectChanged,
             reduxProjectId: state.scratchGui.projectState.projectId,
             reduxProjectTitle: state.scratchGui.projectTitle,
             vm: state.scratchGui.vm
@@ -264,7 +310,8 @@ const ProjectSaverHOC = function (WrappedComponent) {
         onShowCreatingAlert: () => showAlertWithTimeout(dispatch, 'creating'),
         onShowSaveSuccessAlert: () => showAlertWithTimeout(dispatch, 'saveSuccess'),
         onShowSavingAlert: () => showAlertWithTimeout(dispatch, 'saving'),
-        onUpdatedProject: (projectId, loadingState) => dispatch(doneUpdatingProject(projectId, loadingState))
+        onUpdatedProject: (projectId, loadingState) => dispatch(doneUpdatingProject(projectId, loadingState)),
+        setAutoSaveTimeoutId: id => dispatch(setAutoSaveTimeoutId(id))
     });
     // Allow incoming props to override redux-provided props. Used to mock in tests.
     const mergeProps = (stateProps, dispatchProps, ownProps) => Object.assign(
diff --git a/src/reducers/gui.js b/src/reducers/gui.js
index 8fb65a2112e9fac70f4be41fe7297903421e4088..41b5fbce0719880568a8b31dc304f978896b0993 100644
--- a/src/reducers/gui.js
+++ b/src/reducers/gui.js
@@ -20,6 +20,7 @@ import projectTitleReducer, {projectTitleInitialState} from './project-title';
 import restoreDeletionReducer, {restoreDeletionInitialState} from './restore-deletion';
 import stageSizeReducer, {stageSizeInitialState} from './stage-size';
 import targetReducer, {targetsInitialState} from './targets';
+import timeoutReducer, {timeoutInitialState} from './timeout';
 import toolboxReducer, {toolboxInitialState} from './toolbox';
 import vmReducer, {vmInitialState} from './vm';
 import vmStatusReducer, {vmStatusInitialState} from './vm-status';
@@ -51,6 +52,7 @@ const guiInitialState = {
     projectTitle: projectTitleInitialState,
     restoreDeletion: restoreDeletionInitialState,
     targets: targetsInitialState,
+    timeout: timeoutInitialState,
     toolbox: toolboxInitialState,
     vm: vmInitialState,
     vmStatus: vmStatusInitialState
@@ -133,6 +135,7 @@ const guiReducer = combineReducers({
     projectTitle: projectTitleReducer,
     restoreDeletion: restoreDeletionReducer,
     targets: targetReducer,
+    timeout: timeoutReducer,
     toolbox: toolboxReducer,
     vm: vmReducer,
     vmStatus: vmStatusReducer
diff --git a/src/reducers/timeout.js b/src/reducers/timeout.js
new file mode 100644
index 0000000000000000000000000000000000000000..5ed5b7be3c16845abee19fe52f13d3b516acfd88
--- /dev/null
+++ b/src/reducers/timeout.js
@@ -0,0 +1,27 @@
+const SET_AUTOSAVE_TIMEOUT_ID = 'timeout/SET_AUTOSAVE_TIMEOUT_ID';
+
+const initialState = {
+    autoSaveTimeoutId: null
+};
+
+const reducer = function (state, action) {
+    if (typeof state === 'undefined') state = initialState;
+    switch (action.type) {
+    case SET_AUTOSAVE_TIMEOUT_ID:
+        return Object.assign({}, state, {
+            autoSaveTimeoutId: action.id
+        });
+    default:
+        return state;
+    }
+};
+const setAutoSaveTimeoutId = id => ({
+    type: SET_AUTOSAVE_TIMEOUT_ID,
+    id
+});
+
+export {
+    reducer as default,
+    initialState as timeoutInitialState,
+    setAutoSaveTimeoutId
+};
diff --git a/test/unit/util/project-saver-hoc.test.jsx b/test/unit/util/project-saver-hoc.test.jsx
index 4af09047c3bebdd8ae61374fec4dfdcd7d1154a6..90dc63367cffbfe72be92e2371c1ad11a76d8ee7 100644
--- a/test/unit/util/project-saver-hoc.test.jsx
+++ b/test/unit/util/project-saver-hoc.test.jsx
@@ -16,10 +16,16 @@ describe('projectSaverHOC', () => {
     beforeEach(() => {
         store = mockStore({
             scratchGui: {
-                projectState: {}
+                projectChanged: false,
+                projectState: {},
+                projectTitle: 'Scratch Project',
+                timeout: {
+                    autoSaveTimeoutId: null
+                }
             }
         });
         vm = new VM();
+        jest.useFakeTimers();
     });
 
     test('if canSave becomes true when showing a project with an id, project will be saved', () => {
@@ -31,6 +37,7 @@ describe('projectSaverHOC', () => {
                 isShowingWithId
                 canSave={false}
                 isCreatingNew={false}
+                isShowingSaveable={false} // set explicitly because it relies on ownProps.canSave
                 isShowingWithoutId={false}
                 isUpdating={false}
                 loadingState={LoadingState.SHOWING_WITH_ID}
@@ -40,7 +47,8 @@ describe('projectSaverHOC', () => {
             />
         );
         mounted.setProps({
-            canSave: true
+            canSave: true,
+            isShowingSaveable: true
         });
         expect(mockedUpdateProject).toHaveBeenCalled();
     });
@@ -313,4 +321,82 @@ describe('projectSaverHOC', () => {
         });
         expect(mockedShowSavingAlert).toHaveBeenCalled();
     });
+
+    test('if project is changed, it should autosave after interval', () => {
+        const Component = () => <div />;
+        const WrappedComponent = projectSaverHOC(Component);
+        const mockedAutoUpdate = jest.fn(() => Promise.resolve());
+        const mounted = mount(
+            <WrappedComponent
+                canSave
+                isShowingSaveable
+                isShowingWithId
+                loadingState={LoadingState.SHOWING_WITH_ID}
+                store={store}
+                vm={vm}
+                onAutoUpdateProject={mockedAutoUpdate}
+            />
+        );
+        mounted.setProps({
+            projectChanged: true
+        });
+        // Fast-forward until all timers have been executed
+        jest.runAllTimers();
+        expect(mockedAutoUpdate).toHaveBeenCalled();
+    });
+
+    test('if project is changed several times in a row, it should only autosave once', () => {
+        const Component = () => <div />;
+        const WrappedComponent = projectSaverHOC(Component);
+        const mockedAutoUpdate = jest.fn(() => Promise.resolve());
+        const mounted = mount(
+            <WrappedComponent
+                canSave
+                isShowingSaveable
+                isShowingWithId
+                loadingState={LoadingState.SHOWING_WITH_ID}
+                store={store}
+                vm={vm}
+                onAutoUpdateProject={mockedAutoUpdate}
+            />
+        );
+        mounted.setProps({
+            projectChanged: true,
+            reduxProjectTitle: 'a'
+        });
+        mounted.setProps({
+            projectChanged: true,
+            reduxProjectTitle: 'b'
+        });
+        mounted.setProps({
+            projectChanged: true,
+            reduxProjectTitle: 'c'
+        });
+        // Fast-forward until all timers have been executed
+        jest.runAllTimers();
+        expect(mockedAutoUpdate).toHaveBeenCalledTimes(1);
+    });
+
+    test('if project is not changed, it should not autosave after interval', () => {
+        const Component = () => <div />;
+        const WrappedComponent = projectSaverHOC(Component);
+        const mockedAutoUpdate = jest.fn(() => Promise.resolve());
+        const mounted = mount(
+            <WrappedComponent
+                canSave
+                isShowingSaveable
+                isShowingWithId
+                loadingState={LoadingState.SHOWING_WITH_ID}
+                store={store}
+                vm={vm}
+                onAutoUpdateProject={mockedAutoUpdate}
+            />
+        );
+        mounted.setProps({
+            projectChanged: false
+        });
+        // Fast-forward until all timers have been executed
+        jest.runAllTimers();
+        expect(mockedAutoUpdate).not.toHaveBeenCalled();
+    });
 });