diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 7a07c2e4587e68248f4a0f272ff169d7ebde4d45..1a65f141496e8852187bd9528e0411480c550378 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -60,7 +60,7 @@ jobs:
           command: build
           use-cross: ${{ matrix.job.use-cross }}
           toolchain: ${{ matrix.rust }}
-          args: --release --features=all --target ${{ matrix.job.target }}
+          args: --release --features=all,metrics --target ${{ matrix.job.target }}
 
       - name: install strip command
         shell: bash
diff --git a/Cargo.lock b/Cargo.lock
index 3512ee265ff447678772042ebded9e9b54e96322..8e6aadc42ee5982c68e2ba0eb0cb25eb7decff4f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -205,6 +205,18 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "actix-web-prom"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9df3127d20a5d01c9fc9aceb969a38d31a6767e1b48a54d55a8f56c769a84923"
+dependencies = [
+ "actix-web",
+ "futures-core",
+ "pin-project-lite",
+ "prometheus",
+]
+
 [[package]]
 name = "adler"
 version = "1.0.2"
@@ -1992,6 +2004,27 @@ dependencies = [
  "unicode-xid",
 ]
 
+[[package]]
+name = "prometheus"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7f64969ffd5dd8f39bd57a68ac53c163a095ed9d0fb707146da1b27025a3504"
+dependencies = [
+ "cfg-if",
+ "fnv",
+ "lazy_static",
+ "memchr",
+ "parking_lot 0.11.2",
+ "protobuf",
+ "thiserror",
+]
+
+[[package]]
+name = "protobuf"
+version = "2.27.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf7e6d18738ecd0902d30d1ad232c9125985a3422929b16c65517b38adc14f96"
+
 [[package]]
 name = "py_sql"
 version = "1.0.1"
@@ -2389,6 +2422,7 @@ dependencies = [
  "actix-files",
  "actix-rt",
  "actix-web",
+ "actix-web-prom",
  "async-trait",
  "base64",
  "bytes",
@@ -2405,6 +2439,7 @@ dependencies = [
  "mobc-lapin",
  "mobc-redis",
  "openssl",
+ "prometheus",
  "rbatis",
  "rbson",
  "reqwest",
diff --git a/Cargo.toml b/Cargo.toml
index 91500713760c6775fe7c40cdc7f384c38ba91105..56dabcb73cd27a9ac6c7df9305af3b932f562ab6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -30,6 +30,8 @@ strfmt = "^0.1.6"
 thiserror = "^1.0"
 url = "^2.2.2"
 bytes = "^1.1.0"
+prometheus = "^0.13.0"
+actix-web-prom = "^0.6.0"
 
 [dependencies.digest]
 version = "0.10.3"
@@ -133,6 +135,7 @@ default = []
 http_notifier = ["reqwest"]
 redis_info_storage = ["mobc-redis"]
 hashers = ["md-5", "sha1", "sha2", "digest"]
+metrics = []
 
 ### For testing
 test_redis = []
diff --git a/README.md b/README.md
index 94837937d791f0bc8371c4e073c650c3e9f66ecb..a9e9c480d113f79f217b2e429671a6b1db896e96 100644
--- a/README.md
+++ b/README.md
@@ -33,7 +33,7 @@ Preferred version is 1.59.0.
 ```bash
 git clone https://github.com/s3rius/rustus.git
 cd rustus
-cargo install --path . --features=all
+cargo install --path . --features=all,metrics
 ```
 Also you can speedup build by disabling some features.
 
@@ -44,7 +44,8 @@ Available features:
 * `http_notifier` - adds support for notifying about upload status via http protocol;
 * `redis_info_storage` - adds support for storing information about upload in redis database;
 * `hashers` - adds support for checksum verification;
-* `all` - enables all rustus features.
+* `metrics` - adds rustus specific metrics to prometheus endpoint;
+* `all` - enables all rustus features except `metrics`.
 
 All precompiled binaries have all features enabled.
 
diff --git a/deploy/Dockerfile b/deploy/Dockerfile
index 4fca83a51e82e2558cda1acd829e99b23368a90c..acd2de40abef2f59e3b7839c4ac7eacc6e8555e4 100644
--- a/deploy/Dockerfile
+++ b/deploy/Dockerfile
@@ -8,7 +8,7 @@ RUN cargo chef prepare --recipe-path recipe.json
 FROM chef AS builder
 COPY --from=planner /app/recipe.json recipe.json
 # Build dependencies - this is the caching Docker layer!
-RUN cargo chef cook --release --features=all --recipe-path recipe.json
+RUN cargo chef cook --release --features=all,metrics --recipe-path recipe.json
 # Build application
 COPY . .
 RUN cargo build --release --bin rustus --features=all
diff --git a/docs/deploy.md b/docs/deploy.md
index 2583bf2f6503e046be1c2a2e31964e765967211e..b7f243289a7a8e5844136c60ebed6ec494814ada 100644
--- a/docs/deploy.md
+++ b/docs/deploy.md
@@ -9,6 +9,11 @@ Deploying an application is always a challenge. Rustus was made to make deployme
 Since Rustus works with files you have to be careful while scaling it. All rustus instances
 must have access to the same data and info storages.
 
+!!! info
+
+    If you want to track you rustus instances with **prometheus** you can
+    always get metrics at `/metrics` endpoint.
+
 ## Docker compose
 
 ``` yaml title="docker-compose.yml"
diff --git a/docs/index.md b/docs/index.md
index ad367576ea170301aa949f71c8babca455183f29..af486d419bd98637e0d61bea9886cc5ae446f011 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -30,7 +30,7 @@ Preferred version is 1.59.0.
 ```bash
 git clone https://github.com/s3rius/rustus.git
 cd rustus
-cargo install --path . --features=all
+cargo install --path . --features=all,metrics
 ```
 
 Also, you can speedup build by disabling some features.
@@ -42,7 +42,8 @@ Available features:
 * `http_notifier` - adds support for notifying about upload status via `HTTP` protocol;
 * `redis_info_storage` - adds support for storing information about upload in `Redis` database;
 * `hashers` - adds support for checksum verification;
-* `all` - enables all Rustus features.
+* `metrics` - adds rustus specific metrics to prometheus endpoint;
+* `all` - enables all rustus features except `metrics`.
 
 All precompiled binaries have all features enabled.
 
diff --git a/src/errors.rs b/src/errors.rs
index 13ad2ae57a614f7cbae16168fb21eef0185fea64..0f1760231548456baa68761d6c24d88a7a0b8aed 100644
--- a/src/errors.rs
+++ b/src/errors.rs
@@ -65,6 +65,8 @@ pub enum RustusError {
     WrongChecksum,
     #[error("The header value is incorrect")]
     WrongHeaderValue,
+    #[error("Metrics error: {0}")]
+    PrometheusError(#[from] prometheus::Error),
 }
 
 /// This conversion allows us to use `RustusError` in the `main` function.
diff --git a/src/main.rs b/src/main.rs
index 6fc08e66bc997f51c7b6b52a4e4a0b3655bc05cc..26ab5eb6d072cac7361b51deff98cebe836486d2 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -11,14 +11,17 @@ use fern::{
     colors::{Color, ColoredLevelConfig},
     Dispatch,
 };
-use log::LevelFilter;
+use log::{error, LevelFilter};
 
 use config::RustusConf;
 
 use crate::{
-    errors::RustusResult, info_storages::InfoStorage,
-    notifiers::models::notification_manager::NotificationManager, server::rustus_service,
-    state::State, storages::Storage,
+    errors::{RustusError, RustusResult},
+    info_storages::InfoStorage,
+    notifiers::models::notification_manager::NotificationManager,
+    server::rustus_service,
+    state::State,
+    storages::Storage,
 };
 
 mod config;
@@ -72,14 +75,37 @@ fn greeting(app_conf: &RustusConf) {
 /// if the server can't be bound to the
 /// given address.
 #[cfg_attr(coverage, no_coverage)]
-pub fn create_server(state: State) -> Result<Server, std::io::Error> {
+pub fn create_server(state: State) -> RustusResult<Server> {
     let host = state.config.host.clone();
     let port = state.config.port;
     let workers = state.config.workers;
     let state_data: web::Data<State> = web::Data::from(Arc::new(state));
+    let metrics = actix_web_prom::PrometheusMetricsBuilder::new("")
+        .endpoint("/metrics")
+        .build()
+        .map_err(|err| {
+            error!("{}", err);
+            RustusError::Unknown
+        })?;
+    let active_uploads =
+        prometheus::IntGauge::new("active_uploads", "Number of active file uploads")?;
+    let file_sizes = prometheus::Histogram::with_opts(
+        prometheus::HistogramOpts::new("uploads_sizes", "Size of uploaded files in bytes")
+            .buckets(prometheus::exponential_buckets(2., 2., 40)?),
+    )?;
+    #[cfg(feature = "metrics")]
+    {
+        metrics
+            .registry
+            .register(Box::new(active_uploads.clone()))?;
+        metrics.registry.register(Box::new(file_sizes.clone()))?;
+    }
     let mut server = HttpServer::new(move || {
         App::new()
+            .app_data(web::Data::new(active_uploads.clone()))
+            .app_data(web::Data::new(file_sizes.clone()))
             .configure(rustus_service(state_data.clone()))
+            .wrap(metrics.clone())
             .wrap(middleware::Logger::new("\"%r\" \"-\" \"%s\" \"%a\" \"%D\""))
             // Middleware that overrides method of a request if
             // "X-HTTP-Method-Override" header is provided.
diff --git a/src/protocol/core/write_bytes.rs b/src/protocol/core/write_bytes.rs
index fafc64e59c9da415eb623f50cb8fc5178046dcf2..c63e72d669d6449ef87eed60b1038d4427aea8f2 100644
--- a/src/protocol/core/write_bytes.rs
+++ b/src/protocol/core/write_bytes.rs
@@ -14,6 +14,7 @@ pub async fn write_bytes(
     request: HttpRequest,
     bytes: Bytes,
     state: web::Data<State>,
+    #[cfg(feature = "metrics")] active_uploads: web::Data<prometheus::IntGauge>,
 ) -> RustusResult<HttpResponse> {
     // Checking if request has required headers.
     let check_content_type = |val: &str| val == "application/offset+octet-stream";
@@ -109,10 +110,8 @@ pub async fn write_bytes(
     state.info_storage.set_info(&file_info, false).await?;
 
     let mut hook = Hook::PostReceive;
-    let mut keep_alive = true;
     if file_info.length == Some(file_info.offset) {
         hook = Hook::PostFinish;
-        keep_alive = false;
     }
     if state.config.hook_is_active(hook) {
         let message = state
@@ -128,16 +127,15 @@ pub async fn write_bytes(
                 .await
         });
     }
-    if keep_alive {
-        Ok(HttpResponse::NoContent()
-            .insert_header(("Upload-Offset", file_info.offset.to_string()))
-            .keep_alive()
-            .finish())
-    } else {
-        Ok(HttpResponse::NoContent()
-            .insert_header(("Upload-Offset", file_info.offset.to_string()))
-            .finish())
+
+    #[cfg(feature = "metrics")]
+    if hook == Hook::PostFinish {
+        active_uploads.dec();
     }
+
+    Ok(HttpResponse::NoContent()
+        .insert_header(("Upload-Offset", file_info.offset.to_string()))
+        .finish())
 }
 
 #[cfg(test)]
diff --git a/src/protocol/creation/routes.rs b/src/protocol/creation/routes.rs
index 86c8b3440e421694db23a60c5b50164b3bdfc196..52fc2c70f570a39ce50fa156d63dde68e4e19057 100644
--- a/src/protocol/creation/routes.rs
+++ b/src/protocol/creation/routes.rs
@@ -71,6 +71,8 @@ fn get_upload_parts(request: &HttpRequest) -> Vec<String> {
 /// extension is enabled.
 #[allow(clippy::too_many_lines)]
 pub async fn create_file(
+    #[cfg(feature = "metrics")] active_uploads: web::Data<prometheus::IntGauge>,
+    #[cfg(feature = "metrics")] file_sizes: web::Data<prometheus::Histogram>,
     state: web::Data<State>,
     request: HttpRequest,
     bytes: Bytes,
@@ -140,6 +142,16 @@ pub async fn create_file(
     // Create file and get the it's path.
     file_info.path = Some(state.data_storage.create_file(&file_info).await?);
 
+    // Incrementing number of active uploads
+    #[cfg(feature = "metrics")]
+    active_uploads.inc();
+
+    #[cfg(feature = "metrics")]
+    if let Some(length) = file_info.length {
+        #[allow(clippy::cast_precision_loss)]
+        file_sizes.observe(length as f64);
+    }
+
     if file_info.is_final {
         let mut final_size = 0;
         let mut parts_info = Vec::new();
diff --git a/src/routes.rs b/src/routes.rs
index 6a0769234bc6aff0463f9c9769c7b4c327b4d235..fa5463135e3a567e09cf06728cc0a3dd5baec6d0 100644
--- a/src/routes.rs
+++ b/src/routes.rs
@@ -1,14 +1,12 @@
 use actix_web::HttpResponse;
 
-use crate::errors::{RustusError, RustusResult};
-
 /// Default response to all unknown URLs.
 /// All protocol urls can be found
 /// at `crate::protocol::*`.
 #[allow(clippy::unused_async)]
 #[cfg_attr(coverage, no_coverage)]
-pub async fn not_found() -> RustusResult<HttpResponse> {
-    Err(RustusError::FileNotFound)
+pub async fn not_found() -> HttpResponse {
+    HttpResponse::NotFound().finish()
 }
 
 /// Checks that application is accepting connections correctly.
diff --git a/src/state.rs b/src/state.rs
index 45fa622adf357d1195cfcb6e8fc844c9621ba96a..f74a2d6bcd4c4b424e2f7c575c8468dbff4f376e 100644
--- a/src/state.rs
+++ b/src/state.rs
@@ -25,7 +25,7 @@ impl State {
     }
 
     #[cfg(test)]
-    pub async fn from_config(config: RustusConf) -> Self {
+    pub async fn from_config_test(config: RustusConf) -> Self {
         Self {
             config: config.clone(),
             data_storage: Box::new(crate::storages::file_storage::FileStorage::new(
@@ -56,13 +56,13 @@ impl State {
             ]
             .into_iter(),
         );
-        Self::from_config(config).await
+        Self::from_config_test(config).await
     }
 
     #[cfg(test)]
     pub async fn test_clone(&self) -> Self {
         let config = self.config.clone();
-        Self::from_config(config).await
+        Self::from_config_test(config).await
     }
 
     #[cfg(test)]