diff --git a/README.md b/README.md
index 38153d37b766f7239b930286733fd6b7fad2f971..debe2d53d0b0fa81e80aa4935c11d3de05dcd69c 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,12 @@ Below is a description of the config file's properties:
 
 * `cameraSlots`: The camera slots available for this room. Defaults to `4`.
 
+* `notifyPath`: A path to a file which the camera server will append messages to.
+  This is used to notify the controller (usually PULT).
+  The path can either be absolute or relative to the config file but must not start with a tilde.
+
+  If this property is emitted or an empty string is provided, the notify feature will not be used.
+
 ## Stdin-Interface
 
 The camera server is controlled by PULT via its stdin. One could also implement the interface in any other program to manage the CVH-Camera.
@@ -68,6 +74,20 @@ The current state of the camera feeds is saved in the server's memory and will b
 | `hide`                            | Hides the feed of the provided slot. <br/> Note that the feed will still be transmitted to the viewer but is just hidden. By doing that, the feed can be shown again with a very low latency. <br/><br/> **Usage**: `hide <slot>`
 | `show`                            | Shows the feed of the provided slot in case it was hidden. <br/><br/> **Usage**: `show <slot>`
 
+## Notify-Interface
+
+The camera server can notify its controller (usually PULT) by writing to a file which is provided in the config.
+The controller can then read the file and process the messages.
+
+In the case of PULT this file is a named pipe (mkfifo), which works perfectly fine.
+
+This is a list of all sent messages. Note that a newline character `\n` is appended to every message.
+
+| Message                            | Description
+| ---------------------------------- | -----------
+| `new_feed <slot>`                  | Sent after a sender on a slot has started transmitting a feed.
+| `remove_feed <slot>`               | Sent after a sender on a slot has stopped transmitting a feed or the slot is deactivated (which also removes the feed).
+
 ## Socket Traffic
 
 This section describes the socket traffic and the socket.io events that are used.
diff --git a/camera-server/example-config.json b/camera-server/example-config.json
index da5cef680e2d8f5ebc8c688afdc7201c71bfdf07..21696d58dbd4b344a42bc2451e9874d1536f77e5 100644
--- a/camera-server/example-config.json
+++ b/camera-server/example-config.json
@@ -1,4 +1,5 @@
 {
   "port": 5000,
-  "cameraSlots": 4
+  "cameraSlots": 4,
+  "notifyPath": "./path-relative-to-config/or-absolute-path/camera-server-output"
 }
diff --git a/camera-server/src/config/config.ts b/camera-server/src/config/config.ts
index 84ecd9df53d3936dfb407df5272e6964a17a79e9..5863a51c6053f410438ceac1384f4be875b994ee 100644
--- a/camera-server/src/config/config.ts
+++ b/camera-server/src/config/config.ts
@@ -4,13 +4,13 @@ import * as path from 'path';
 interface Config {
     port: number;
     cameraSlots: number;
+    notifyPath: string;
 }
 
 // Required to access config with config[key]
 // But only an object of type Config should be returned
 interface IndexableConfig extends Config {
-    // Change type once non-number values are added
-    [key: string]: number;
+    [key: string]: number | string;
 }
 
 let configPath = process.env.CONFIG_PATH;
@@ -22,7 +22,7 @@ if (configPath) {
         fileContent = fs.readFileSync(configPath).toString();
     } catch (err) {
         console.log(
-            `Could not read config at ${path.resolve(configPath)}:`,
+            `Error: Could not read config at ${path.resolve(configPath)}:`,
             err
         );
         console.log('Using default values');
@@ -33,7 +33,8 @@ if (configPath) {
 
 const indexableConfig: IndexableConfig = {
     port: 5000,
-    cameraSlots: 4
+    cameraSlots: 4,
+    notifyPath: ''
 };
 
 if (fileContent) {
@@ -59,12 +60,12 @@ if (fileContent) {
                     );
                 }
             } else {
-                console.log(`Unknown property ${key} in config`);
+                console.log(`Error: Unknown property ${key} in config`);
             }
         });
     } else {
         console.log(
-            `Config at ${path.resolve(
+            `Error: Config at ${path.resolve(
                 configPath!
             )} is malformed - using default values`
         );
@@ -72,6 +73,12 @@ if (fileContent) {
 }
 
 const config = indexableConfig as Config;
+if (config.notifyPath && !path.isAbsolute(config.notifyPath) && configPath) {
+    config.notifyPath = path.resolve(
+        path.dirname(configPath),
+        config.notifyPath
+    );
+}
 
 console.log('Using config:', config);
 
diff --git a/camera-server/src/io-interface/handlers/output-handlers.ts b/camera-server/src/io-interface/handlers/output-handlers.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ad81cd3374ec004ca949404efef8f950ae654b08
--- /dev/null
+++ b/camera-server/src/io-interface/handlers/output-handlers.ts
@@ -0,0 +1,55 @@
+import * as fs from 'fs';
+
+import { config } from '../../config/config';
+
+// Saves the amount of blocking append calls
+// in case the named pipe is not read by PULT
+let blockingAppendCalls = 0;
+
+const timeoutTime = 5000;
+
+const printTimeoutMessage = (notifyPath: string) => {
+    console.log(
+        `Error: Controller did not read file at path '${notifyPath}' for ${timeoutTime}ms`
+    );
+};
+
+const notifyController = (message: string) => {
+    const { notifyPath } = config;
+    if (notifyPath) {
+        if (fs.existsSync(notifyPath)) {
+            const timeoutId = setTimeout(
+                printTimeoutMessage.bind(null, notifyPath),
+                timeoutTime
+            );
+            console.log(
+                `Notifying controller about message '${message}' using file at path '${notifyPath}'`
+            );
+            blockingAppendCalls += 1;
+            fs.appendFile(notifyPath, message + '\n', (err) => {
+                if (err) {
+                    console.log(
+                        `Error: Tried to notify controller about message '${message}' but could not write to path '${notifyPath}' - Error:`,
+                        err
+                    );
+                }
+                blockingAppendCalls -= 1;
+                clearTimeout(timeoutId);
+            });
+        } else {
+            console.log(
+                `Error: Tried to notify controller about message '${message}' using file at path '${notifyPath}' which does not exist`
+            );
+        }
+    }
+};
+
+export const isBlocking = () => blockingAppendCalls !== 0;
+
+export const notifyNewFeed = (slot: number) => {
+    notifyController(`new_feed ${slot}`);
+};
+
+export const notifyRemoveFeed = (slot: number) => {
+    notifyController(`remove_feed ${slot}`);
+};
diff --git a/camera-server/src/socket-io/handlers/common-handlers.ts b/camera-server/src/socket-io/handlers/common-handlers.ts
index d3b56691f50c5f2502530aaa2afca3a841b9470b..39d4e5386c0ff9fb7bedf2204d272cc1a6f8490b 100644
--- a/camera-server/src/socket-io/handlers/common-handlers.ts
+++ b/camera-server/src/socket-io/handlers/common-handlers.ts
@@ -1,19 +1,25 @@
 import { socketIO } from '../socket-io';
 import { cameraSlotState } from '../../state/camera-slot-state';
 import { CommandDescriptor } from '../../models/command-descriptor';
+import {
+    notifyNewFeed,
+    notifyRemoveFeed
+} from '../../io-interface/handlers/output-handlers';
 
 export const emitNewFeed = (slot: number) => {
-    const cameraState = cameraSlotState[slot];
+    const slotState = cameraSlotState[slot];
     socketIO.emit('new_feed', {
         slot,
-        feedId: cameraState.feedId,
-        visibility: cameraState.visibility,
-        geometry: cameraState.geometry
+        feedId: slotState.feedId,
+        visibility: slotState.visibility,
+        geometry: slotState.geometry
     });
+    notifyNewFeed(slot);
 };
 
 export const emitRemoveFeed = (slot: number) => {
     socketIO.emit('remove_feed', { slot });
+    notifyRemoveFeed(slot);
 };
 
 export const handleQueryState = (fn: Function) => {
@@ -26,12 +32,12 @@ export const handleQueryState = (fn: Function) => {
         };
     } = {};
     for (let i = 0; i < cameraSlotState.length; i++) {
-        const cameraState = cameraSlotState[i];
-        if (cameraState.feedActive) {
+        const slotState = cameraSlotState[i];
+        if (slotState.feedActive) {
             response[i] = {
-                feedId: cameraState.feedId,
-                visibility: cameraState.visibility,
-                geometry: cameraState.geometry
+                feedId: slotState.feedId,
+                visibility: slotState.visibility,
+                geometry: slotState.geometry
             };
         }
     }
diff --git a/camera-server/src/util/cleanup.ts b/camera-server/src/util/cleanup.ts
index 5cc867a4fff2a245f63c13f0d2357f968917c951..0a78da277e82507dc1291fe4700d9944b376a5ae 100644
--- a/camera-server/src/util/cleanup.ts
+++ b/camera-server/src/util/cleanup.ts
@@ -1,19 +1,29 @@
 import { socketIO } from '../socket-io/socket-io';
+import { isBlocking } from '../io-interface/handlers/output-handlers';
 
 interface ExitHandlerOptions {
     cleanup?: boolean;
     exit?: boolean;
 }
 
-const cleanup = () => {
-    console.log('cleanup');
-    socketIO.emit('remove_all_feeds');
-};
-
-const exitHandler = (options: ExitHandlerOptions, exitCode: number) => {
-    if (options.cleanup) cleanup();
-    if (exitCode || exitCode === 0) console.log(exitCode);
-    if (options.exit) process.exit();
+const exitHandler = (
+    options: ExitHandlerOptions,
+    exitCode: string | number
+) => {
+    if (options.cleanup) {
+        console.log('cleanup');
+        socketIO.emit('remove_all_feeds');
+    }
+    if (exitCode || exitCode === 0) {
+        console.log('Exit code:', exitCode);
+        if (exitCode === 0 && isBlocking()) {
+            console.log('Aborting process due to blocking file append');
+            process.abort();
+        }
+    }
+    if (options.exit) {
+        process.exit();
+    }
 };
 
 export const registerCleanupLogic = () => {