diff --git a/docs/05-API-Docs.md b/docs/05-API-Docs.md
index 3a672a2..654b625 100644
--- a/docs/05-API-Docs.md
+++ b/docs/05-API-Docs.md
@@ -115,8 +115,20 @@ https://example.com/path/to/api.php?action=device-data&device-id=18&reading-type
 https://example.com/path/to/api.php?action=device-data&device-id=18&reading-type=PM25&start=2019-06-13T00:00:00&end=now&format=csv
 ```
 
+## device-data-recent
+> Gets a given number of the most recent readings for a specific device.
 
+Parameter			| Type		| Meaning
+--------------------|-----------|---------------------
+`device-id`			| int		| The id of the device to get data for.
+`reading-type`		| string	| The type of reading to obtain data for.
+`count`				| int		| The number of recent readings to return.
+`format`            | string    | Optional. Specifies the format that the response will be returned in. Valid values: `json`, `csv`. Default: `json`.
 
+```
+https://example.com/path/to/api.php?action=device-data-recent&device-id=21&reading-type=PM25&count=5
+https://example.com/path/to/api.php?action=device-data-recent&device-id=36&reading-type=humidity&count=30
+```
 
 ## changelog
 > Gets the changelog as a fragment of HTML.
diff --git a/logic/Actions/DeviceDataRecent.php b/logic/Actions/DeviceDataRecent.php
new file mode 100644
index 0000000..2468cee
--- /dev/null
+++ b/logic/Actions/DeviceDataRecent.php
@@ -0,0 +1,125 @@
+<?php
+
+namespace AirQuality\Actions;
+
+use \SBRL\TomlConfig;
+use \SBRL\ResponseEncoder;
+use \AirQuality\Repositories\IMeasurementDataRepository;
+use \AirQuality\Repositories\IMeasurementTypeRepository;
+use \AirQuality\ApiResponseSender;
+
+use \AirQuality\Validator;
+use \AirQuality\PerfFormatter;
+
+/**
+ * Action that retrieves recent data for a device.
+ */
+class DeviceDataRecent implements IAction {
+	/** @var TomlConfig */
+	private $settings;
+	
+	/** @var IMeasurementDataRepository */
+	private $measurement_repo;
+	/** @var IMeasurementTypeRepository */
+	private $type_repo;
+	
+	/** @var ApiResponseSender */
+	private $sender;
+	
+	/** @var Validator */
+	private $validator;
+	
+	public function __construct(
+		TomlConfig $in_settings,
+		IMeasurementDataRepository $in_measurement_repo,
+		IMeasurementTypeRepository $in_type_repo,
+		ApiResponseSender $in_sender) {
+		$this->settings = $in_settings;
+		$this->measurement_repo = $in_measurement_repo;
+		$this->type_repo = $in_type_repo;
+		$this->sender = $in_sender;
+		
+		$this->validator = new Validator($_GET);
+	}
+	
+	public function handle() : bool {
+		global $start_time;
+		
+		$start_handle = microtime(true);
+		
+		// 1: Validate params
+		$this->validator->is_numberish("device-id");
+		$this->validator->exists("reading-type");
+		$this->validator->is_max_length("reading-type", 256);
+		$this->validator->is_numberish("count");
+		$this->validator->is_min("count", 0);
+		$this->validator->is_max("count", $this->settings->get("limits.device_data_recent.max_rows"));
+		
+		if(!empty($_GET["format"]))
+			$this->validator->is_preset_value("format", ["json", "csv"], 406);
+		
+		$this->validator->run();
+		
+		$format = $_GET["format"] ?? "json";
+		
+		$count = intval($_GET["count"]);
+		
+		$reading_type_id = $this->type_repo->get_id($_GET["reading-type"]);
+		
+		if($reading_type_id == null) {
+			$this->sender->send_error_plain(
+				400, "Error: That reading type is invalid.", [
+					[ "x-time-taken", PerfFormatter::format_perf_data($start_time, $start_handle, null) ]
+				]
+			);
+			return false;
+		}
+		
+		// 2: Pull data from database
+		$data = $this->measurement_repo->get_recent_readings(
+			intval($_GET["device-id"]),
+			$reading_type_id,
+			$count
+		);
+		
+		// 2.5: Validate data from database
+		if(empty($data)) {
+			http_response_code(404);
+			header("content-type: text/plain");
+			header("x-time-taken: " . PerfFormatter::format_perf_data($start_time, $start_handle, null));
+			echo("Error: No data has been recorded from the device id for that measurement type in the selected time scale or it doesn't exist.");
+			return false;
+		}
+		
+		
+		// 3: Serialise data
+		
+		// FUTURE: Refactor this into an AbstractAction or something, but the careful not to shift too much work there
+		// We might want to consider a HttpResponse container or something if we can find a decent PSR implementation
+		$start_encode = microtime(true);
+		$response_type = "application/octet-stream";
+		$response_suggested_filename = "data-" . date(\DateTime::ATOM) . "";
+		$response = null;
+		switch($format) {
+			case "json":
+				$response_type = "application/json";
+				$response_suggested_filename .= ".json";
+				$response = json_encode($data);
+				break;
+			case "csv":
+				$response_type = "text/csv";
+				$response_suggested_filename .= ".csv";
+				$response = ResponseEncoder::encode_csv($data);
+				break;
+		}
+		
+		
+		// 4: Send response
+		header("content-length: " . strlen($response));
+		header("content-type: $response_type");
+		header("content-disposition: inline; filename=$response_suggested_filename");
+		header("x-time-taken: " . PerfFormatter::format_perf_data($start_time, $start_handle, $start_encode));
+		echo($response);
+		return true;
+	}
+}
diff --git a/logic/Actions/IAction.php b/logic/Actions/IAction.php
index 1aef415..b136d18 100644
--- a/logic/Actions/IAction.php
+++ b/logic/Actions/IAction.php
@@ -2,6 +2,13 @@
 
 namespace AirQuality\Actions;
 
+/**
+ * Interface that defines the functionality of an action that requests can be routed to.
+ */
 interface IAction {
+    /**
+     * Handles the a request for the action.
+     * @return bool Whether the request was handled successfully or not.
+     */
     public function handle() : bool;
 }
diff --git a/logic/Repositories/IMeasurementDataRepository.php b/logic/Repositories/IMeasurementDataRepository.php
index 7220fb3..ad79a99 100644
--- a/logic/Repositories/IMeasurementDataRepository.php
+++ b/logic/Repositories/IMeasurementDataRepository.php
@@ -29,4 +29,13 @@ interface IMeasurementDataRepository {
 	 */
 	public function get_readings_by_device(int $device_id, int $type_id, \DateTime $start, \DateTime $end, int $average_seconds = 1);
 	
+	/**
+	 * Retrieves a given number of the most recent readings for a specific device.
+	 * @param	int		$device_id	The id of the device to get recent readings for.
+	 * @param	int		$type_id	The id of the reading type to fetch data for.
+	 * @param	int		$count		The number of readings to return.
+	 * @return	array	The requested data.
+	 */
+	public function get_recent_readings(int $device_id, int $type_id, int $count);
+	
 }
diff --git a/logic/Repositories/MariaDBDeviceRepository.php b/logic/Repositories/MariaDBDeviceRepository.php
index 20a1d73..44a3ce1 100644
--- a/logic/Repositories/MariaDBDeviceRepository.php
+++ b/logic/Repositories/MariaDBDeviceRepository.php
@@ -35,7 +35,10 @@ class MariaDBDeviceRepository implements IDeviceRepository {
 	 */
 	private $database;
 	
-	/** Function that gets a static variable by it's name. Useful in preparing SQL queries. */
+	/** 
+	 * Function that gets a static variable by it's name. Useful in preparing SQL queries.
+	 * @var callable
+	 */
 	private $get_static;
 	
 	function __construct(\AirQuality\Database $in_database) {
diff --git a/logic/Repositories/MariaDBMeasurementDataRepository.php b/logic/Repositories/MariaDBMeasurementDataRepository.php
index cc9523c..e4a4d24 100644
--- a/logic/Repositories/MariaDBMeasurementDataRepository.php
+++ b/logic/Repositories/MariaDBMeasurementDataRepository.php
@@ -35,10 +35,23 @@ class MariaDBMeasurementDataRepository implements IMeasurementDataRepository {
 	 */
 	private $database;
 	
-	/** Function that gets a static variable by it's name. Useful in preparing SQL queries. */
+	/** 
+	 * Function that gets a static variable by it's name. Useful in preparing SQL queries.
+	 * @var callable
+	 */
 	private $get_static;
+	/**
+	 * Function that gets a static variable by it's name & class. Useful in preparing SQL queries.
+	 * @var callable
+	 */
 	private $get_static_extra;
 	
+	/**
+	 * Creates a new reepository instance.
+	 * It is suggested that PHP-DI is utilised for instantiation via the associated interface.
+	 * @param Database   $in_database The database connection to use.
+	 * @param TomlConfig $in_settings The settings to operate with.
+	 */
 	function __construct(Database $in_database, TomlConfig $in_settings) {
 		$this->database = $in_database;
 		$this->settings = $in_settings;
@@ -52,6 +65,8 @@ class MariaDBMeasurementDataRepository implements IMeasurementDataRepository {
 	public function get_readings_by_date(\DateTime $datetime, int $type_id) {
 		$s = $this->get_static;
 		$o = $this->get_static_extra;
+		
+		// OPTIMIZE: I think we can drastically improve the performance of this query by pre-calculating the start & end dates of the window
 		return $this->database->query(
 			"SELECT
 				{$s("table_name_values")}.{$s("column_values_value")},
@@ -106,7 +121,7 @@ class MariaDBMeasurementDataRepository implements IMeasurementDataRepository {
 	
 	public function get_readings_by_device(int $device_id, int $type_id, \DateTime $start, \DateTime $end, int $average_seconds = 1) {
 		if($average_seconds < 1)
-			throw new Exception("Error: average_seconds must be greater than 1, but '$average_seconds' was specified.");
+			throw new \Exception("Error: average_seconds must be greater than 1, but '$average_seconds' was specified.");
 		$s = $this->get_static;
 		return $this->database->query(
 			"SELECT
@@ -144,4 +159,35 @@ class MariaDBMeasurementDataRepository implements IMeasurementDataRepository {
 			]
 		)->fetchAll();
 	}
+	
+	public function get_recent_readings(int $device_id, int $type_id, int $count) {
+		if($count <= 0) return [];
+		
+		$s = $this->get_static;
+		return $this->database->query(
+			"SELECT
+				AVG({$s("table_name_values")}.{$s("column_values_value")}) AS {$s("column_values_value")},
+				MIN({$s("table_name_values")}.{$s("column_values_reading_id")}) AS {$s("column_values_reading_id")},
+				
+				MIN(COALESCE(
+					{$s("table_name_metadata")}.{$s("column_metadata_recordedon")},
+					{$s("table_name_metadata")}.{$s("column_metadata_storedon")}
+				)) AS datetime
+			FROM {$s("table_name_values")}
+			JOIN {$s("table_name_metadata")} ON
+				{$s("table_name_metadata")}.{$s("column_metadata_id")} = {$s("table_name_values")}.{$s("column_values_reading_id")}
+			WHERE
+				{$s("table_name_metadata")}.{$s("column_metadata_device_id")} = :device_id AND
+				{$s("table_name_values")}.{$s("column_values_reading_type")} = :reading_type
+			ORDER BY COALESCE(
+				{$s("table_name_metadata")}.{$s("column_metadata_recordedon")},
+				{$s("table_name_metadata")}.{$s("column_metadata_storedon")}
+			)
+			LIMIT :count;", [
+				"device_id" => $device_id,
+				"reading_type" => $type_id,
+				"count" => $count
+			]
+		)->fetchAll();
+	}
 }
diff --git a/settings.default.toml b/settings.default.toml
index 9a71230..aced295 100644
--- a/settings.default.toml
+++ b/settings.default.toml
@@ -45,3 +45,12 @@ max-age = 2592000 # 30 days
 # time-step. Ideally, this value should be half of the actual data recording 
 # interval.
 max_reading_timediff = 180
+
+[limits]
+# Various tunable limits.
+
+[limits.device_data_recent]
+# The device-data-recent action
+
+# The max. number of rows allowed in a single request
+max_rows = 1000
