## Module 6: Understanding Request Unit (RU)

![demo_203-tidb-x-ru.jpg 85%](https://lab-static.pingcap.com/images/2025/11/27/0bf1a7a4c5d0d9c7661042ae329bf1e089a59290.jpg)

This module will help you build a deeper understanding of Request Units (RUs) by exploring how different database operations consume them. So, what is RU? With TiDB Cloud, you don't need to manage TiDB and TiKV nodes in TiDB Cloud clusters. Instead, billing is determined by actual storage usage and the number of read and write Request Units (RUs) consumed. Essentially, an RU is a weighted sum representing the resources consumed by your SQL requests.

The table below provides a rough estimation of RUs in TiDB Cloud. Don't worry if you don't fully understand what these values mean—this won't affect the lab steps.

| RU Type    | Rough Definition                                                                     |
| ---------- | :----------------------------------------------------------------------------------- |
| 1 Read RU  | `0.5 x # of batch PointGet + 0.125 x # of PointGet + # of 64K data + 0.3 x CPU (ms)` |
| 1 Write RU | `(0.5 x # of batch PointGet + 0.5 x # of PointGet + # of 2K data ) x 3`              |

You can configure minimum and maximum RU per seconds limits to automatically control your cluster's auto-scaling boundaries. Refer to the table below for more details.

| Subscription Plan     | Starter                        | Essential               |
| :-------------------- | -----------------------------: | ----------------------: |
| Free usage eligible   | 5 GiB & 50 million RU monthly  | No                      |
| Minimal RU per second | 0                              | 2K                      |
| Maximum RU per second | No guarantee                   | 100K                    |
| Cluster autoscaling   | No guarantee                   | 2k ~ 100K RU per second |
| Availability          | 99.9% Zonal                    | 99.99% Regional         |

![payg.jpg 50%](https://lab-static.pingcap.com/images/2025/12/12/1077d4a1ea93f3ba5a38f93250ba42f30ee81bdb.jpg)

The [Request Unit (RU)](https://www.pingcap.com/tidb-cloud-starter-pricing-details/) serves as a unified, abstract metric in TiDB for quantifying system resources—such as CPU/IO—consumed by individual database requests. Make full use of the free TiDB Cloud Starter cluster to test and estimate the actual RU usage of your SQL statements to help inform your decisions.

This module will guide you step by step through the process of how to obtain RU information.

### Step 1. Create the testing tables

- On [Terminal 1](ssh://monitor1-ssh#1), connect to TiDB if you are not already connected.

  ```bash
  mysql \
  --comments -u '${LAB:SERVERLESS_CLUSTER_USERNAME}' -p'${LAB:SERVERLESS_CLUSTER_PASSWORD}' \
  -h ${LAB:SERVERLESS_CLUSTER_HOST} -P ${LAB:SERVERLESS_CLUSTER_PORT} \
  --ssl-mode=VERIFY_IDENTITY --ssl-ca=/etc/ssl/certs/ca-certificates.crt
  ```

  ```output
  mysql>
  ```

- Create the test tables `tbl_ru_1` and `tbl_ru_2` in the `${LAB:SERVERLESS_CLUSTER_DATABASE_NAME}` schema. The DDL statements below show that both tables have identical definitions.

  ```sql
  CREATE DATABASE IF NOT EXISTS ${LAB:SERVERLESS_CLUSTER_DATABASE_NAME};
  USE ${LAB:SERVERLESS_CLUSTER_DATABASE_NAME};

  DROP TABLE IF EXISTS tbl_ru_1;
  CREATE TABLE tbl_ru_1 (
  `ID` bigint PRIMARY KEY AUTO_RANDOM,
  `pState` varchar (100) NOT NULL,
  `cState` tinyint (4) DEFAULT NULL,
  `created_at` DATETIME,
  `updated_at` DATETIME,
  `txt` varchar(64) DEFAULT 'TTLTEST',
  KEY `pcStateIndex`(`pState`, `cState`)
  ) PRE_SPLIT_REGIONS=4;

  DROP TABLE IF EXISTS tbl_ru_2;
  CREATE TABLE tbl_ru_2 (
  `ID` bigint PRIMARY KEY AUTO_RANDOM,
  `pState` varchar (100) NOT NULL,
  `cState` tinyint (4) DEFAULT NULL,
  `created_at` DATETIME,
  `updated_at` DATETIME,
  `txt` varchar(64) DEFAULT 'TTLTEST',
   KEY `pcStateIndex` (`pState`, `cState`)
   ) PRE_SPLIT_REGIONS=4;
  ```

  ```output
  ...
  Query OK, 0 rows affected, 1 warning (0.28 sec)
  ```

### Step 2. Become familiar with the Monitoring Console

- Go to the [Clusters page](https://tidbcloud.com/project/clusters) in your TiDB Cloud console and click the name of your lab cluster. If you started the lab by choosing `Create New Cluster`, the cluster name will be `${LAB:SERVERLESS_CLUSTER_NAME}`.

![Demo-RU-001.png 85%](https://lab-static.pingcap.com/images/2025/11/24/9c016ddc77e89cb9d9ae0669890d36cb316f6d20.png)

- Click on the cluster name `${LAB:SERVERLESS_CLUSTER_NAME}`, then select `Overview` in the upper left corner.

![Demo-RU-002.png 85%](https://lab-static.pingcap.com/images/2025/11/24/8d064cdda27759df1640171cbf92f7d340e4364d.png)

- Expand the `Monitoring` tab, select the `Diagnosis` item. Here, users can monitor executed SQL statements and the number of RUs consumed by each SQL statement.

![Demo-RU-003.png 85%](https://lab-static.pingcap.com/images/2025/11/24/586655532161734f86ee41c826cd99221b3533ce.png)

- Next, expand the `Monitoring` tab, select the `Metrics` item, and then choose the `Cluster Status` tab. This area allows users to monitor the current Request Unit(RU) consumption per second and QPS metrics for the database cluster.

![Demo-RU-004.png 85%](https://lab-static.pingcap.com/images/2025/11/24/1be07d68448a466052182b702096201f01e83fbc.png)

- Please keep this page open, as you will use it later to monitor the RU consumption of your SQL statements.

### Step 3. Run the insert workload with single worker

- On [Terminal 1](ssh://monitor1-ssh#1), exit to OS prompt.

  ```sql
  EXIT;
  ```

- On [Terminal 1](ssh://monitor1-ssh#1), create a Python virtual environment.

  ```bash
  python3 -m venv ~/.venv
  ~/.venv/bin/pip install mysql-connector-python prettytable
  ```

- On [Terminal 1](ssh://monitor1-ssh#1), create a Python script that executes the writing operation in a single thread.

  ```bash
  cat > demo_insert_single_thread.py <<'EOF'
  import mysql.connector
  from prettytable import PrettyTable
  import datetime
  import time
  import threading
  import sys

  config = {
  'user': '${LAB:SERVERLESS_CLUSTER_USERNAME}',
  'password': '${LAB:SERVERLESS_CLUSTER_PASSWORD}',
  'host': '${LAB:SERVERLESS_CLUSTER_HOST}',
  'database': '${LAB:SERVERLESS_CLUSTER_DATABASE_NAME}',
  'raise_on_warnings': False,
  'port': ${LAB:SERVERLESS_CLUSTER_PORT},
  }

  def database_worker(thread_id):
     try:
         num_loop = 100;

         # Establishing the connection
         conn = mysql.connector.connect(**config)
         # Creating a cursor object
         cursor = conn.cursor(prepared=True)

         for n in range(num_loop):
             queries = ["INSERT INTO tbl_ru_1(pState, cState, created_at, updated_at) VALUES (ELT(1 + FLOOR(RAND() * 9), 's1', 's2', 's3', 's4', 's5', 's6', 's7', 's8', 's9', 's0'), 1 + FLOOR(RAND() * 2), NOW(), NOW());"
                     ]
             num_query = 1;
             cursor.execute("BEGIN")

             for i in range(num_query):
                 cursor.execute(queries[i])

             cursor.execute("COMMIT")

             if n % 10 == 0 and n != 0:
                 print("%d rows have been inserted." % n)

         print("%d rows have been inserted." % num_loop)

         try:
             cursor.close()
             conn.close()
         except Error as e:
             pass

     except (Error, MySQLInterfaceError) as connect_err:
         print(f"Thread {thread_id}: Connection or execution error: {connect_err}")
  def main():
     num_threads = 1

     print(f"Starting {num_threads} database worker threads...")

     # Create and start threads
     threads = []
     for i in range(num_threads):
         thread = threading.Thread(
             target=database_worker,
             args=(i + 1,),
             daemon=True
         )
         threads.append(thread)
         thread.start()
         time.sleep(0.1)

     print(f"All {num_threads} threads started successfully!")

     try:
         # Keep the main thread alive - wait for threads to complete (they run indefinitely)
         for thread in threads:
             thread.join()
     except KeyboardInterrupt:
         print("\nReceived interrupt signal. Stopping all threads...")
         sys.exit(0)

  if __name__ == "__main__":
     main()
  EOF
  ls -l demo_insert_single_thread.py
  ```

  ```output
  -rw-rw-r-- 1 ubuntu ubuntu 2250 Nov 20 11:35 demo_insert_single_thread.py
  ```

- On [Terminal 1](ssh://monitor1-ssh#1), run the Python script, in this python script, we start a thread that inserts a row of random data into the table `tbl_ru_1`, for a total of 100 rows. This step will take about 2 minutes.

  ```bash
  ~/.venv/bin/python demo_insert_single_thread.py
  ```

  ```output
  Starting 1 database worker threads...
  All 1 threads started successfully!
  10 rows have been inserted.
  20 rows have been inserted.
  ...
  100 rows have been inserted.
  ```

### Step 4. Investigate the RU consumption

- Navigate to the Overview page of your `${LAB:SERVERLESS_CLUSTER_NAME}` cluster. Expand the left-hand `Monitoring` tab, select the `Metrics` item, and then choose the `Cluster Status` tab. Set the time range to `Past 15 minutes` and the refresh rate to `30s`. After waiting 2-3 minutes, you will observe the recent write activity reflected in both the `Query Per Second` and `Request Units` panels. You can then record the RU consumption per second observed during the single-threaded write operation.

![Demo-RU-005.png 85%](https://lab-static.pingcap.com/images/2025/11/24/37a67e7f484d93f87bf7684636f6b09a4f13da29.png)

- Next, expand the `Monitoring` tab, select the `Diagnosis` item, and choose the `SQL Statement` tab. Set the time range to `Past 15 minutes` and filter the SQL command type to `Insert`. After 1-3 minutes, the previously executed SQL statements will appear in the results. You should record the `Executions Count`, `Total RU`, and `Mean RU` values for the insert statements.
- `Mean RU` represents the RU consumed by a single execution of the corresponding SQL command. In our case, it's 2.55.
- `Total RU` indicates the total Request Units consumed by all executions of a particular SQL command during the selected time range. In our example, this value is 255.00, which corresponds to the 100 insert operations performed in Step 4.

![Demo-RU-006.png 85%](https://lab-static.pingcap.com/images/2025/11/24/6ac06a5976dfb3ae4df234937f28b02e31973b25.png)

- Here's an interesting observation: In a linearly scaling distributed system like TiDB, predicting RU consumption is straightforward. Let's prove it in the next 2 Steps.

### Step 5. Run the insert workload with 10x workers

- On [Terminal 1](ssh://monitor1-ssh#1), create another Python script that executes the insert operation with 10 workers.

  ```bash
  cat > demo_insert_multi_thread.py <<'EOF'
  import mysql.connector
  from prettytable import PrettyTable
  import datetime
  import time
  import threading
  import sys

  config = {
  'user': '${LAB:SERVERLESS_CLUSTER_USERNAME}',
  'password': '${LAB:SERVERLESS_CLUSTER_PASSWORD}',
  'host': '${LAB:SERVERLESS_CLUSTER_HOST}',
  'database': '${LAB:SERVERLESS_CLUSTER_DATABASE_NAME}',
  'raise_on_warnings': False,
  'port': ${LAB:SERVERLESS_CLUSTER_PORT},
  }

  def database_worker(thread_id):
     try:
         num_loop = 100;

         # Establishing the connection
         conn = mysql.connector.connect(**config)

         # Creating a cursor object
         cursor = conn.cursor(prepared=True)

         for n in range(num_loop):
             queries = ["INSERT INTO tbl_ru_2(pState, cState, created_at, updated_at) VALUES (ELT(1 + FLOOR(RAND() * 9), 's1', 's2', 's3', 's4', 's5', 's6', 's7', 's8', 's9', 's0'), 1 + FLOOR(RAND() * 2), NOW(), NOW());"
                     ]
             num_query = 1;
             cursor.execute("BEGIN")
             for i in range(num_query):
                 cursor.execute(queries[i])
             cursor.execute("COMMIT")
             if n % 10 == 0 and n != 0:
                 print("Thread %d: %d rows have been inserted." % (thread_id, n))
         print("Thread %d: %d rows have been inserted." % (thread_id, num_loop))

         try:
             cursor.close()
             conn.close()
         except Error as e:
             pass

     except (Error, MySQLInterfaceError) as connect_err:
         print(f"Thread {thread_id}: Connection or execution error: {connect_err}")
  def main():
     num_threads = 10

     print(f"Starting {num_threads} database worker threads...")

     # Create and start threads
     threads = []
     for i in range(num_threads):
         thread = threading.Thread(
             target=database_worker,
             args=(i + 1,),
             daemon=True
         )
         threads.append(thread)
         thread.start()
         time.sleep(0.1)

     print(f"All {num_threads} threads started successfully!")

     try:
         # Keep the main thread alive - wait for threads to complete (they run indefinitely)
         for thread in threads:
             thread.join()
     except KeyboardInterrupt:
         print("\nReceived interrupt signal. Stopping all threads...")
         sys.exit(0)

  if __name__ == "__main__":
     main()
  EOF
  ls -l demo_insert_multi_thread.py
  ```

  ```output
  -rw-rw-r-- 1 ubuntu ubuntu 2212 Nov 21 09:03 demo_insert_multi_thread.py
  ```

- Run the Python script, in this python script, we start 10 threads. Each of them inserts a row of random data into the table `tbl_ru_2` whose definition is the same as `tbl_ru_1`, for a total of 100 rows.

  ```bash
  ~/.venv/bin/python demo_insert_multi_thread.py
  ```

  ```output
  Starting 10 database worker threads...
  ...
  Thread 9: 100 rows have been inserted.
  Thread 6: 100 rows have been inserted.
  Thread 7: 100 rows have been inserted.
  Thread 8: 100 rows have been inserted.
  ```

### Step 6. Investigate the RU consumption again

- Navigate to the Overview page of your `${LAB:SERVERLESS_CLUSTER_NAME}` cluster. Expand the left-hand `Monitoring` tab, select the `Metrics` item, and then choose the `Cluster Status` tab. Set the time range to `Past 15 minutes` and the refresh rate to `30s`. After waiting 2-3 minutes, you will observe the recent write activity reflected in both the `Query Per Second` and `Request Units` panels. You can then record the RU consumption per second observed during the single-threaded write operation.You can then record the RU consumption per second during the multi-threaded write operation.

![Demo-RU-007.png 85%](https://lab-static.pingcap.com/images/2025/11/24/a3d5f12c2f36c28700d1ed35e56f85e9b78d98f0.png)

- Next, expand the `Monitoring` tab, select the `Diagnosis` item, and choose the `SQL Statement` tab. Set the time range to `Past 15 minutes` and filter the SQL command type to `Insert`. After 1-3 minutes, the previously executed SQL statements will appear in the results. You should record the `Executions Count`, `Total RU`, and `Mean RU` values for the insert statements.

![Demo-RU-008.png 85%](https://lab-static.pingcap.com/images/2025/11/24/460a92179f90a203c65482ae697f7265a242ffae.png)

- When you run the same workload with 10 concurrent workers, the mean RU will stay approximately the same, but the total RU consumption will be 10 times that of a single worker. In our example, the mean RU remains around 2.55, and the total RU is about 2.55K.

### Step 7. Use EXPLAIN to get SQL statement RU information

- The Monitoring Console may have a delay of 2-3 minutes, but `EXPLAIN ANALYZE` allows you to instantly obtain statistics on the RU consumption of your SQL queries.

- On [Terminal 1](ssh://monitor1-ssh#1), connect to TiDB.

  ```bash
  mysql \
  --comments -u '${LAB:SERVERLESS_CLUSTER_USERNAME}' -p'${LAB:SERVERLESS_CLUSTER_PASSWORD}' \
  -h ${LAB:SERVERLESS_CLUSTER_HOST} -P ${LAB:SERVERLESS_CLUSTER_PORT} \
  --ssl-mode=VERIFY_IDENTITY --ssl-ca=/etc/ssl/certs/ca-certificates.crt \
  -D ${LAB:SERVERLESS_CLUSTER_DATABASE_NAME}
  ```

- Run the following query with `explain analyze`.

  ```sql
  PAGER less -S
  EXPLAIN ANALYZE
  SELECT MAX(created_at)
  FROM tbl_ru_1;
  ```

- When reviewing the output, use the right and left arrow keys on your keyboard to locate the column called `execution info`. Pay attention to the value following `RU:` on the first row—this indicates the actual number of RUs consumed by this query. In the given example, it is `RU:0.581812`. You might see a different value, but the difference will not be significant.

  ```output
  ... -----------+---------------+----------------------------------------------------->
  ...  task      | access object | execution info                                      >
  ... -----------+---------------+----------------------------------------------------->
  ...  root      |               | time:1.39ms, loops:2, RU:0.581812
  ```

- Press the `Q` key on your keyboard to exit the output.

### Step 8. Cleanup

- On [Terminal 1](ssh://monitor1-ssh#1), drop the tables `tbl_ru_1` and `tbl_ru_2` under the ${LAB:SERVERLESS_CLUSTER_DATABASE_NAME} schema.

  ```sql
  NOPAGER;
  USE ${LAB:SERVERLESS_CLUSTER_DATABASE_NAME};
  DROP TABLE tbl_ru_1;
  DROP TABLE tbl_ru_2;
  ```

  ```output
  Query OK, 0 rows affected (0.55 sec)
  ...
  Query OK, 0 rows affected (0.63 sec)
  ```

- With a solid grasp of RUs, you will be able to assess the RU consumption of SQL statements, compare TiDB Cloud subscription plans using RU metrics, and select the subscription plan that best matches your workload requirements. Visit this [document](https://www.pingcap.com/tidb-cloud-starter-pricing-details/) for more detailed information about RUs and Pricing. For now, please proceed to the next module.
