diff --git a/www-server/pom.xml b/www-server/pom.xml
index 59c6abd8acbecfc52041be9cc9cc83d05095b0a3..c153b7cd7987a9cd67a5e5c76a59d36054687238 100644
--- a/www-server/pom.xml
+++ b/www-server/pom.xml
@@ -16,6 +16,7 @@
 
   <properties>
     <mockito-core.version>5.5.0</mockito-core.version>
+    <sava.version>0.0.1-SNAPSHOT</sava.version>
     <swagger.version>2.2.19</swagger.version>
     <checkstyle.config.location>file://${basedir}/../config/sun_checks.xml</checkstyle.config.location>
   </properties>
@@ -154,6 +155,12 @@
       <artifactId>postgresql</artifactId>
       <version>42.7.0</version>
     </dependency>
+    <!-- SAVA -->
+    <dependency>
+      <groupId>fr.inrae.agroclim</groupId>
+      <artifactId>sava-core-jakarta</artifactId>
+      <version>${sava.version}</version>
+    </dependency>
     <!-- Tests -->
     <dependency>
       <groupId>com.h2database</groupId>
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/MetricsServlet.java b/www-server/src/main/java/fr/agrometinfo/www/server/MetricsServlet.java
new file mode 100644
index 0000000000000000000000000000000000000000..ef334068563c3e1175300fdef87df24d310562bd
--- /dev/null
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/MetricsServlet.java
@@ -0,0 +1,17 @@
+package fr.agrometinfo.www.server;
+
+
+import fr.agroclim.sava.core.MetricsBasicAuthServlet;
+import jakarta.servlet.annotation.WebServlet;
+
+/**
+ * Servlet to using SAVA to provide metrics.
+ */
+@WebServlet("/metrics")
+public class MetricsServlet extends MetricsBasicAuthServlet  {
+    /**
+     * UID.
+     */
+    private static final long serialVersionUID = 1003353995341179826L;
+
+}
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/dao/DaoHibernate.java b/www-server/src/main/java/fr/agrometinfo/www/server/dao/DaoHibernate.java
index 5c33432169a148074bee347f84c512180d67e166..dc1e1b7898b45a82adbc06d4e1f9f69c8da4eb3c 100644
--- a/www-server/src/main/java/fr/agrometinfo/www/server/dao/DaoHibernate.java
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/dao/DaoHibernate.java
@@ -1,9 +1,14 @@
 package fr.agrometinfo.www.server.dao;
 
+import java.math.BigDecimal;
+import java.sql.Connection;
+import java.sql.SQLException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 
+import org.hibernate.engine.spi.SessionImplementor;
+
 import jakarta.persistence.NoResultException;
 import jakarta.persistence.Query;
 import jakarta.persistence.TypedQuery;
@@ -17,6 +22,37 @@ import lombok.extern.log4j.Log4j2;
  */
 @Log4j2
 public abstract class DaoHibernate<T> {
+    /**
+     * Get the class name of JDBC driver.
+     *
+     * @param em Entity manager
+     * @return name of JDBC driver
+     */
+    protected static final String getDriver(final ScopedEntityManager em) {
+        final SessionImplementor sessionImp = (SessionImplementor) em.getDelegate();
+        try {
+            final Connection conn = sessionImp.getJdbcConnectionAccess().obtainConnection();
+            return conn.getMetaData().getDriverName();
+        } catch (final SQLException e) {
+            return "";
+        }
+    }
+
+    /**
+     * Checks if the database is connected by a PostgreSQL, useful to call specific
+     * SQL.
+     *
+     * @param em Entity manager
+     * @return if the SGBD is PostgreSQL
+     */
+    protected static final boolean isPostgreSQLDriver(final ScopedEntityManager em) {
+        final var driverName = getDriver(em);
+        if (driverName == null || driverName.isBlank()) {
+            return false;
+        }
+        return driverName.startsWith("PostgreSQL");
+    }
+
     /**
      * Related class.
      */
@@ -125,13 +161,34 @@ public abstract class DaoHibernate<T> {
         }
     }
 
+    /**
+     * Find the first result.
+     *
+     * @param <U> entity class
+     * @param sql SQL statement
+     * @param params parameters for the JPQL statement
+     * @return the found entity or null if not entity matches
+     */
+    @SuppressWarnings("unchecked")
+    protected final <U> U findFirstScalarWithNamedParameters(final String sql, final Map<String, Object> params) {
+        try (ScopedEntityManager em = getScopedEntityManager()) {
+            final Query query = em.createNativeQuery(sql);
+            if (params != null) {
+                params.forEach(query::setParameter);
+            }
+            return (U) query.getSingleResult();
+        } catch (final NoResultException e) {
+            return null;
+        }
+    }
+
     /**
      * Find the first result.
      *
      * @param <U>    entity class
      * @param stmt   Jakarta Persistence Query Language statement
      * @param params parameters for the JPQL statement
-     * @param clazz2  entity class
+     * @param clazz2 entity class
      * @return the found entity or null if not entity matches
      */
     protected final <U> U findOneByJPQL(final String stmt, final Map<String, Object> params, final Class<U> clazz2) {
@@ -146,6 +203,37 @@ public abstract class DaoHibernate<T> {
         }
     }
 
+    /**
+     * @return name of current schema.
+     */
+    public String getSchemaName() {
+        final String sql = """
+                SELECT table_schema FROM information_schema.tables WHERE LOWER(table_name) = 'indicator'
+                """;
+        return findFirstScalar(sql, null);
+    }
+
+    /**
+     * See https://www.postgresql.org/docs/current/functions-admin.html.
+     *
+     * @param schema schema name (usually public)
+     * @return size of all content of a schema, in bytes
+     */
+    public BigDecimal getSchemaSize(final String schema) {
+        try (ScopedEntityManager em = getScopedEntityManager()) {
+            if (isPostgreSQLDriver(em)) {
+                final String sql = "SELECT "
+                        + "SUM(pg_total_relation_size(CONCAT(quote_ident(schemaname), '.', quote_ident(tablename)))) "
+                        + "FROM pg_catalog.pg_tables WHERE schemaname=:schema";
+                final Query query = em.createNativeQuery(sql);
+                query.setParameter("schema", schema);
+                return (BigDecimal) query.getSingleResult();
+            } else {
+                throw new UnsupportedOperationException("Database driver not handled! " + getDriver(em));
+            }
+        }
+    }
+
     /**
      * @return Proxy'ed EntityManager to handle transactions.
      */
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/dao/IndicatorDao.java b/www-server/src/main/java/fr/agrometinfo/www/server/dao/IndicatorDao.java
index baf6581838cb9198c871e340f8e0496dc2906bbe..03b4d9cfc0951033d8390a9b7aba33e5995cbc7e 100644
--- a/www-server/src/main/java/fr/agrometinfo/www/server/dao/IndicatorDao.java
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/dao/IndicatorDao.java
@@ -1,5 +1,6 @@
 package fr.agrometinfo.www.server.dao;
 
+import java.math.BigDecimal;
 import java.util.List;
 
 import fr.agrometinfo.www.server.model.Indicator;
@@ -23,4 +24,13 @@ public interface IndicatorDao {
      */
     Indicator findByCodeAndPeriod(String code, String periodCode);
 
+    /**
+     * @return size of all content of the current schema, in bytes
+     */
+    BigDecimal getCurrentSchemaSize();
+
+    /**
+     * @return name of current schema.
+     */
+    String getSchemaName();
 }
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/dao/IndicatorDaoHibernate.java b/www-server/src/main/java/fr/agrometinfo/www/server/dao/IndicatorDaoHibernate.java
index edeec7aff47dc879394f80d30491760b7be5f040..feec09391f4e940202f11c0eb9d21a1c3e77d969 100644
--- a/www-server/src/main/java/fr/agrometinfo/www/server/dao/IndicatorDaoHibernate.java
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/dao/IndicatorDaoHibernate.java
@@ -1,5 +1,6 @@
 package fr.agrometinfo.www.server.dao;
 
+import java.math.BigDecimal;
 import java.util.Map;
 
 import fr.agrometinfo.www.server.model.Indicator;
@@ -29,9 +30,14 @@ public class IndicatorDaoHibernate extends DaoHibernate<Indicator> implements In
      * @return found or null
      */
     @Override
-    public Indicator findByCodeAndPeriod(@NotNull final String code, @NotNull final String periodCode) {
+    public final Indicator findByCodeAndPeriod(@NotNull final String code, @NotNull final String periodCode) {
         final var jpql = "SELECT t FROM Indicator AS t WHERE t.code=:code AND t.period.code=:periodCode";
         return super.findOneByJPQL(jpql, Map.of("code", code, "periodCode", periodCode), Indicator.class);
     }
 
+    @Override
+    public final BigDecimal getCurrentSchemaSize() {
+        return super.getSchemaSize(super.getSchemaName());
+    }
+
 }
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/scheduled/BackgroundJobManager.java b/www-server/src/main/java/fr/agrometinfo/www/server/scheduled/BackgroundJobManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..ff82b7a5f859f9bb5d1e3711ea963885fa3a127c
--- /dev/null
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/scheduled/BackgroundJobManager.java
@@ -0,0 +1,130 @@
+package fr.agrometinfo.www.server.scheduled;
+
+import java.time.Duration;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import fr.agroclim.sava.core.SavaUtils;
+import fr.agrometinfo.www.server.dao.IndicatorDao;
+import fr.agrometinfo.www.server.dao.PersistenceManager;
+import jakarta.inject.Inject;
+import jakarta.servlet.ServletContextEvent;
+import jakarta.servlet.ServletContextListener;
+import jakarta.servlet.annotation.WebListener;
+import lombok.extern.log4j.Log4j2;
+
+/**
+ * Manager for scheduled tasks.
+ *
+ * Full JavaEE simply uses javax.ejb.Schedule annotation.
+ *
+ * @author Olivier Maury
+ */
+@WebListener
+@Log4j2
+public class BackgroundJobManager implements ServletContextListener {
+
+    /**
+     * Number of seconds in a day.
+     */
+    private static final int DAY_IN_SECONDS = 60 * 60 * 24;
+
+    /**
+     * Number of seconds in an hour.
+     */
+    private static final int HOUR_IN_SECONDS = 60 * 60;
+
+    /**
+     * @param hour   the hour-of-day of delay, from 0 to 23
+     * @param minute the minute-of-hour of delay, from 0 to 59
+     * @param second the second-of-minute of delay, from 0 to 59
+     * @return delays in seconds until the next hour:minute
+     */
+    static long initialDelay(final int hour, final int minute, final int second) {
+        LocalDateTime next = LocalDateTime.of(LocalDate.now(), LocalTime.of(hour, minute, second));
+        final LocalDateTime now = LocalDateTime.now();
+        if (next.isBefore(now)) {
+            next = next.plusDays(1);
+        }
+        return Duration.between(now, next).getSeconds();
+    }
+
+    /**
+     * DAO providing database details.
+     */
+    @Inject
+    private IndicatorDao indicatorDao;
+
+    /**
+     * Scheduler for tasks.
+     */
+    private ScheduledExecutorService scheduler;
+
+    @Override
+    public final void contextDestroyed(final ServletContextEvent event) {
+        LOGGER.traceEntry();
+        PersistenceManager.getInstance().closeEntityManagerFactory();
+        scheduler.shutdownNow();
+    }
+
+    @Override
+    public final void contextInitialized(final ServletContextEvent event) {
+        scheduler = Executors.newSingleThreadScheduledExecutor();
+        scheduleEveryPeriod(this::updateHourlyMetrics, HOUR_IN_SECONDS);
+        scheduleEveryDayAt(this::updateHourlyMetrics, 0, 0, 0);
+        initMetrics();
+        updateHourlyMetrics();
+    }
+
+    /**
+     * Define metrics using SAVA before use.
+     */
+    private void initMetrics() {
+        SavaUtils.addGauge("schema_size", "Database schema size, in bytes", "schema_name");
+    }
+
+    /**
+     * Creates and executes a periodic action.
+     *
+     * @param runnable
+     *            the task to execute
+     * @param hour
+     *            the hour of schedule
+     * @param minute
+     *            the minute of schedule
+     * @param second
+     *            the second of schedule
+     */
+    private void scheduleEveryDayAt(final Runnable runnable, final int hour, final int minute, final int second) {
+        final long initDelay = initialDelay(hour, minute, second);
+        LOGGER.trace("Scheduling {} after {}s at {}:{}:{}", runnable.getClass().getCanonicalName(), initDelay, hour,
+                minute, second);
+        scheduler.scheduleAtFixedRate(runnable, initDelay, DAY_IN_SECONDS, TimeUnit.SECONDS);
+    }
+
+    /**
+     * Creates and executes a periodic action after waiting one period.
+     *
+     * @param runnable
+     *            the task to execute
+     * @param period
+     *            period between 2 runs (in seconds)
+     */
+    private void scheduleEveryPeriod(final Runnable runnable, final int period) {
+        LOGGER.trace("Scheduling {} after {}s.", runnable.getClass().getCanonicalName(), period);
+        scheduler.scheduleAtFixedRate(runnable, period, period, TimeUnit.SECONDS);
+    }
+
+    /**
+     * Update metrics using SAVA.
+     */
+    private void updateHourlyMetrics() {
+        final var schemaName = indicatorDao.getSchemaName();
+        final var schemaSize = indicatorDao.getCurrentSchemaSize();
+        SavaUtils.setGaugeValue("schema_size", schemaSize.doubleValue(), schemaName);
+    }
+}
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/scheduled/package-info.java b/www-server/src/main/java/fr/agrometinfo/www/server/scheduled/package-info.java
new file mode 100644
index 0000000000000000000000000000000000000000..98d6cdefdb923b5b37b7b28c86f20f25d5cc8525
--- /dev/null
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/scheduled/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Scheduled jobs.
+ */
+package fr.agrometinfo.www.server.scheduled;