
Measuring the Impact of Round-Trip Latency Between the App and the DB on Response Time with TiDB Cloud Starter
This page has been translated by machine translation. View original
Introduction
When you use a cloud DB such as TiDB Cloud, a network sits between the app and the DB. Many people probably worry that even if the DB processes queries quickly, communication latency will slow down responses.
Every time you send a SQL statement, the round-trip time between the app and the DB is added. Even if the SQL processing itself is short, if you write code that sends SQL sequentially many times within one request, the round-trip time should accumulate as many times as the number of queries.
In this article, I vary the round-trip latency between the app and TiDB Cloud Starter and the number of SQL statements executed per request, and measure how the app's response time changes.
The increase in response time was determined not by the size of the round-trip latency but by the number of sequential round trips to the DB per request. Consolidating SQL from 6 statements to 2 reduced the response time by 66.9% even under a condition with a round-trip time of about 100 ms.
What is TiDB Cloud
TiDB Cloud is a managed service for TiDB, a MySQL-compatible distributed database. Starter is a pay-as-you-go plan, and instances that meet the conditions receive a free tier.
Test environment
- TiDB Cloud Starter (AWS Tokyo region)
- TiDB v8.5.3
- macOS
- Python 3.13
- PyMySQL 2.2.8
- Toxiproxy 2.12.0
- Measurement date: October 14, 2026
Intended audience
- Developers considering adopting or placing a cloud DB
- Those who want to know the relationship between how DB access is written and response time
References
- Connect to Your TiDB Cloud Starter or Essential Instance (PingCAP)
- Limitations and Quotas of TiDB Cloud Starter and Essential (PingCAP)
- Toxiproxy (Shopify)
Test method
The subject is the order history screen of an e-commerce site.
The process of returning a customer's latest 5 orders and the line items of each order is treated as one request. I implemented this process in two ways that differ in how SQL is sent.
| Implementation | Number of SQL statements | Description |
|---|---|---|
| Sequential fetch | 6 | After fetching 5 orders, fetch the line items for each order |
| Batch fetch | 2 | After fetching 5 orders, fetch the line items for all 5 in a single query using an IN clause |
Sequential fetch is the so-called N+1 style. I confirmed at every measurement that the two implementations return the same result.
Tables and data
I create orders for orders and order_items for order line items.
Each of 1,000 customers has 10 orders, and each order has 5 line items. That is 10,000 orders and 50,000 line items.
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
customer_id INT NOT NULL,
ordered_at DATETIME NOT NULL,
KEY idx_customer (customer_id, ordered_at DESC)
);
CREATE TABLE order_items (
order_id BIGINT NOT NULL,
line_no TINYINT NOT NULL,
product_name VARCHAR(64) NOT NULL,
qty TINYINT NOT NULL,
unit_price INT NOT NULL,
PRIMARY KEY (order_id, line_no)
);
The two implementations
Sequential fetch retrieves the orders and then sends a line-item SQL statement for each order.
def order_page_sequential(cur, customer_id, sql_sec=None):
"""注文 5 件を取り、注文ごとに明細を取る (SQL 6 回)。"""
sql_sec = [] if sql_sec is None else sql_sec
orders, sec = timed(cur, ORDERS_SQL, (customer_id,))
sql_sec.append(sec)
page = []
for order_id, ordered_at in orders:
items, sec = timed(cur, ITEMS_ONE_SQL, (order_id,))
sql_sec.append(sec)
page.append({"id": order_id, "ordered_at": str(ordered_at), "items": [list(i[1:]) for i in items]})
return page
Batch fetch passes the order IDs together to an IN clause and retrieves the line items in one query.
def order_page_batched(cur, customer_id, sql_sec=None):
"""注文 5 件を取り、明細を IN 句で 1 回で取る (SQL 2 回)。"""
sql_sec = [] if sql_sec is None else sql_sec
orders, sec = timed(cur, ORDERS_SQL, (customer_id,))
sql_sec.append(sec)
ids = [o[0] for o in orders]
items, sec = timed(cur, ITEMS_IN_SQL.format(",".join(["%s"] * len(ids))), ids)
sql_sec.append(sec)
by_order = {}
for item in items:
by_order.setdefault(item[0], []).append(list(item[1:]))
return [{"id": oid, "ordered_at": str(at), "items": by_order.get(oid, [])} for oid, at in orders]
How latency is added
Instead of changing the distance between the app and the DB, I placed the Toxiproxy proxy between my local Mac and TiDB Cloud and added round-trip latency artificially. Half of the latency is set on upstream and half on downstream, and the added round-trip latency has three levels: 0 ms, 20 ms, and 80 ms.
docker run -d --rm --name tidb-latency-proxy \
-p 127.0.0.1:8474:8474 -p 127.0.0.1:13306:13306 \
ghcr.io/shopify/toxiproxy:2.12.0
python latency_bench.py proxy
In the script, only the TCP connection destination is pointed at the proxy, and the TLS certificate and hostname are verified as those of TiDB Cloud.
Measurement procedure
All times are elapsed times measured on the client. The response time of one request is the time from the start of processing until the result of the last SQL statement is received and the result is assembled.
In the main comparison experiment, I reused a single connection, performed 3 warm-up runs at each latency, and then ran each of the two implementations 30 times. The execution order of the implementations was swapped on every run, and the order of latencies was also swapped every 10 runs. Before and after each latency, I ran SELECT 1 30 times to check the round-trip time.
In addition, I also ran an experiment that alternated 30 times each between creating a connection for every request and reusing a pre-created connection for the batch fetch process. The time to create a connection includes the TCP connection, the TLS handshake, and authentication.
python latency_bench.py setup
python latency_bench.py bench 0,20,80 > bench.jsonl
python latency_bench.py conn 0,80 > conn.jsonl
python latency_bench.py summarize bench.jsonl conn.jsonl
Results are shown as the median of 30 runs and quartiles (the lower 25% value and the upper 25% value). The connection information is the values confirmed on the Connect screen of the TiDB Cloud console, passed via environment variables.
latency_bench.py
"""TiDB Cloud Starter で、通信遅延がアプリの 1 リクエストの応答時間に与える影響を計測する。
接続情報は環境変数 TIDB_HOST, TIDB_PORT, TIDB_USER, TIDB_PASSWORD から読む。
遅延は Toxiproxy (管理 API: TOXIPROXY_API、既定 http://127.0.0.1:8474) で加える。
python latency_bench.py setup # テーブル作成とデータ投入
python latency_bench.py proxy # Toxiproxy にプロキシ tidb を登録
python latency_bench.py probe 0,20,80 # 接続時間と SELECT 1 の往復時間
python latency_bench.py check 0,20,80 # 2 つの処理の結果一致と、遅延による増分の確認
python latency_bench.py bench 0,20,80 # 本計測: 遅延 × 逐次取得・一括取得 (1 行 1 試行の JSON)
python latency_bench.py conn 0,80 # 本計測: 接続を毎回作る場合と使い回す場合
python latency_bench.py summarize bench.jsonl conn.jsonl # 中央値と増分の集計
"""
import json
import os
import socket
import ssl
import statistics
import sys
import time
import urllib.request
import pymysql
DB = "latency_lab"
PROXY_NAME = "tidb"
PROXY_LISTEN = ("127.0.0.1", 13306)
API = os.environ.get("TOXIPROXY_API", "http://127.0.0.1:8474")
SETUP = [
f"CREATE DATABASE IF NOT EXISTS {DB}",
f"USE {DB}",
"CREATE TABLE IF NOT EXISTS digit (n INT PRIMARY KEY)",
"INSERT IGNORE INTO digit VALUES (0),(1),(2),(3),(4),(5),(6),(7),(8),(9)",
"""CREATE TABLE IF NOT EXISTS orders (
id BIGINT PRIMARY KEY,
customer_id INT NOT NULL,
ordered_at DATETIME NOT NULL,
KEY idx_customer (customer_id, ordered_at DESC)
)""",
"""CREATE TABLE IF NOT EXISTS order_items (
order_id BIGINT NOT NULL,
line_no TINYINT NOT NULL,
product_name VARCHAR(64) NOT NULL,
qty TINYINT NOT NULL,
unit_price INT NOT NULL,
PRIMARY KEY (order_id, line_no)
)""",
]
# 顧客 1,000 人 × 注文 10 件 = 注文 10,000 件、注文ごとに明細 5 件
LOAD = [
"""INSERT INTO orders
SELECT x.id, 1 + MOD(x.id - 1, 1000),
DATE_ADD('2026-09-01', INTERVAL x.id MINUTE)
FROM (
SELECT 1 + a.n + 10*b.n + 100*c.n + 1000*d.n AS id
FROM digit a CROSS JOIN digit b CROSS JOIN digit c CROSS JOIN digit d
) AS x""",
"""INSERT INTO order_items
SELECT o.id, e.n + 1, CONCAT('product-', MOD(o.id * 7 + e.n, 500)),
1 + MOD(e.n, 3), 100 + MOD(o.id + e.n, 900)
FROM orders AS o CROSS JOIN digit AS e WHERE e.n < 5""",
]
ORDERS_SQL = """SELECT id, ordered_at FROM orders
WHERE customer_id = %s ORDER BY ordered_at DESC LIMIT 5"""
ITEMS_ONE_SQL = """SELECT order_id, line_no, product_name, qty, unit_price
FROM order_items WHERE order_id = %s ORDER BY line_no"""
ITEMS_IN_SQL = """SELECT order_id, line_no, product_name, qty, unit_price
FROM order_items WHERE order_id IN ({}) ORDER BY order_id, line_no"""
def tls_context():
ctx = ssl.create_default_context()
ctx.check_hostname = True
ctx.verify_mode = ssl.CERT_REQUIRED
return ctx
def connect(via_proxy, database=DB):
"""TiDB に接続する。via_proxy なら TCP を Toxiproxy に向け、TLS のホスト名は TiDB のものを検証する。"""
conn = pymysql.connect(
host=os.environ["TIDB_HOST"],
port=int(os.environ.get("TIDB_PORT", "4000")),
user=os.environ["TIDB_USER"],
password=os.environ["TIDB_PASSWORD"],
database=database,
ssl=tls_context(),
autocommit=True,
defer_connect=True,
)
sock = socket.create_connection(PROXY_LISTEN, timeout=30) if via_proxy else None
conn.connect(sock=sock)
return conn
def api(method, path, body=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(API + path, data=data, method=method,
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req) as res:
return res.read()
def set_delay(rtt_ms):
"""往復で rtt_ms だけ遅れるよう、上り・下りに半分ずつ latency を設定する。0 なら外す。"""
for stream in ("upstream", "downstream"):
name = f"latency_{stream}"
try:
api("DELETE", f"/proxies/{PROXY_NAME}/toxics/{name}")
except urllib.error.HTTPError:
pass
if rtt_ms > 0:
api("POST", f"/proxies/{PROXY_NAME}/toxics",
{"name": name, "type": "latency", "stream": stream,
"attributes": {"latency": rtt_ms // 2, "jitter": 0}})
def setup():
cur = connect(False, None).cursor()
for sql in SETUP:
cur.execute(sql)
cur.execute("SELECT COUNT(*) FROM orders")
if cur.fetchone()[0] == 0:
for sql in LOAD:
cur.execute(sql)
cur.execute("SELECT (SELECT COUNT(*) FROM orders), (SELECT COUNT(*) FROM order_items)")
print("orders, order_items:", cur.fetchone())
def proxy():
try:
api("DELETE", f"/proxies/{PROXY_NAME}")
except urllib.error.HTTPError:
pass
api("POST", "/proxies", {"name": PROXY_NAME, "listen": "0.0.0.0:13306",
"upstream": f"{os.environ['TIDB_HOST']}:4000", "enabled": True})
print(api("GET", f"/proxies/{PROXY_NAME}").decode())
def timed(cur, sql, args=None):
start = time.perf_counter()
cur.execute(sql, args)
rows = cur.fetchall()
return rows, time.perf_counter() - start
def order_page_sequential(cur, customer_id, sql_sec=None):
"""注文 5 件を取り、注文ごとに明細を取る (SQL 6 回)。"""
sql_sec = [] if sql_sec is None else sql_sec
orders, sec = timed(cur, ORDERS_SQL, (customer_id,))
sql_sec.append(sec)
page = []
for order_id, ordered_at in orders:
items, sec = timed(cur, ITEMS_ONE_SQL, (order_id,))
sql_sec.append(sec)
page.append({"id": order_id, "ordered_at": str(ordered_at), "items": [list(i[1:]) for i in items]})
return page
def order_page_batched(cur, customer_id, sql_sec=None):
"""注文 5 件を取り、明細を IN 句で 1 回で取る (SQL 2 回)。"""
sql_sec = [] if sql_sec is None else sql_sec
orders, sec = timed(cur, ORDERS_SQL, (customer_id,))
sql_sec.append(sec)
ids = [o[0] for o in orders]
items, sec = timed(cur, ITEMS_IN_SQL.format(",".join(["%s"] * len(ids))), ids)
sql_sec.append(sec)
by_order = {}
for item in items:
by_order.setdefault(item[0], []).append(list(item[1:]))
return [{"id": oid, "ordered_at": str(at), "items": by_order.get(oid, [])} for oid, at in orders]
def summary(values_ms):
q1, median, q3 = statistics.quantiles(values_ms, n=4, method="inclusive")
return {"median": round(median, 2), "q1": round(q1, 2), "q3": round(q3, 2)}
def probe(delays, n=30):
"""接続確立の時間と SELECT 1 の往復時間を、プロキシなしと各遅延設定で測る。"""
for label, via_proxy, delay in [("direct", False, 0)] + [(f"proxy+{d}", True, d) for d in delays]:
if via_proxy:
set_delay(delay)
connect_ms, select1_ms = [], []
for _ in range(5):
start = time.perf_counter()
conn = connect(via_proxy)
connect_ms.append((time.perf_counter() - start) * 1000)
conn.close()
conn = connect(via_proxy)
cur = conn.cursor()
for _ in range(3):
timed(cur, "SELECT 1")
for _ in range(n):
select1_ms.append(timed(cur, "SELECT 1")[1] * 1000)
conn.close()
print(json.dumps({"path": label, "connect_ms": summary(connect_ms),
"select1_ms": summary(select1_ms)}))
set_delay(0)
def check(delays, n=5):
"""2 つの処理の結果一致と、各遅延設定での応答時間を数回だけ確かめる。"""
for delay in delays:
set_delay(delay)
conn = connect(True)
cur = conn.cursor()
seq_ms, bat_ms = [], []
for i in range(n):
customer_id = 1 + (i * 37) % 1000
start = time.perf_counter()
a = order_page_sequential(cur, customer_id)
seq_ms.append((time.perf_counter() - start) * 1000)
start = time.perf_counter()
b = order_page_batched(cur, customer_id)
bat_ms.append((time.perf_counter() - start) * 1000)
assert a == b and len(a) == 5 and all(len(o["items"]) == 5 for o in a), customer_id
conn.close()
print(json.dumps({"delay_ms": delay, "identical": True,
"sequential_ms": [round(x, 1) for x in seq_ms],
"batched_ms": [round(x, 1) for x in bat_ms]}))
set_delay(0)
def select1_series(cur, n=30):
return summary([timed(cur, "SELECT 1")[1] * 1000 for _ in range(n)])
def emit(row):
print(json.dumps(row), flush=True)
def bench(delays, warmup=3, rounds=30, block=10):
"""遅延 × 逐次取得・一括取得。1 本の接続を使い回し、実装の実行順は周回ごと、遅延の順序はまとまりごとに入れ替える。"""
conn = connect(True)
cur = conn.cursor()
pages = {"sequential": order_page_sequential, "batched": order_page_batched}
for delay in delays:
set_delay(delay)
for i in range(warmup):
for page in pages.values():
page(cur, 1 + i)
emit({"kind": "select1", "delay_ms": delay, "when": "before", **select1_series(cur)})
for start in range(0, rounds, block):
order = delays if (start // block) % 2 == 0 else list(reversed(delays))
for delay in order:
set_delay(delay)
for i in range(start, start + block):
customer_id = 1 + (i * 37) % 1000
names = ["sequential", "batched"] if i % 2 == 0 else ["batched", "sequential"]
for name in names:
sql_sec = []
t0 = time.perf_counter()
pages[name](cur, customer_id, sql_sec)
total = time.perf_counter() - t0
emit({"kind": "page", "delay_ms": delay, "round": i, "version": name,
"customer_id": customer_id, "total_ms": round(total * 1000, 2),
"sql_ms": [round(x * 1000, 2) for x in sql_sec]})
for delay in delays:
set_delay(delay)
emit({"kind": "select1", "delay_ms": delay, "when": "after", **select1_series(cur)})
set_delay(0)
conn.close()
def conn_bench(delays, warmup=3, rounds=30):
"""一括取得の処理を、接続を毎回作る場合 (new) と使い回す場合 (reuse) で交互に実行する。"""
for delay in delays:
set_delay(delay)
reuse = connect(True)
reuse_cur = reuse.cursor()
for i in range(warmup + rounds):
customer_id = 1 + (i * 37) % 1000
for mode in (["new", "reuse"] if i % 2 == 0 else ["reuse", "new"]):
t0 = time.perf_counter()
if mode == "new":
conn = connect(True)
connect_sec = time.perf_counter() - t0
order_page_batched(conn.cursor(), customer_id)
total = time.perf_counter() - t0
conn.close()
else:
connect_sec = 0.0
order_page_batched(reuse_cur, customer_id)
total = time.perf_counter() - t0
if i >= warmup:
emit({"kind": "conn", "delay_ms": delay, "round": i - warmup, "mode": mode,
"total_ms": round(total * 1000, 2), "connect_ms": round(connect_sec * 1000, 2)})
reuse.close()
set_delay(0)
def summarize(bench_path, conn_path):
rows = [json.loads(line) for line in open(bench_path)]
pages = [r for r in rows if r["kind"] == "page"]
delays = sorted({r["delay_ms"] for r in pages})
for version in ("sequential", "batched"):
base = None
for delay in delays:
ms = summary([r["total_ms"] for r in pages if r["version"] == version and r["delay_ms"] == delay])
base = ms["median"] if base is None else base
print(f"{version:10} +{delay:>2} ms: median {ms['median']:7.1f} "
f"(q1 {ms['q1']:6.1f}, q3 {ms['q3']:6.1f}) increase {ms['median'] - base:6.1f}")
rows = [json.loads(line) for line in open(conn_path)]
for delay in sorted({r["delay_ms"] for r in rows}):
for mode in ("new", "reuse"):
target = [r for r in rows if r["delay_ms"] == delay and r["mode"] == mode]
total = summary([r["total_ms"] for r in target])
connect_ms = summary([r["connect_ms"] for r in target])
print(f"conn {mode:5} +{delay:>2} ms: median {total['median']:7.1f} "
f"(q1 {total['q1']:6.1f}, q3 {total['q3']:6.1f}) connect {connect_ms['median']:6.1f}")
if __name__ == "__main__":
command = sys.argv[1]
if command == "summarize":
summarize(sys.argv[2], sys.argv[3])
sys.exit()
if command in ("probe", "check", "bench", "conn"):
{"probe": probe, "check": check, "bench": bench, "conn": conn_bench}[command](
[int(d) for d in sys.argv[2].split(",")])
else:
{"setup": setup, "proxy": proxy}[command]()
Results
Checking the round-trip time
For each latency setting, I measured the round-trip time of SELECT 1. The values are the medians before and after the measurement.
{"kind": "select1", "delay_ms": 0, "when": "before", "median": 14.83, "q1": 13.86, "q3": 16.69}
{"kind": "select1", "delay_ms": 20, "when": "before", "median": 37.68, "q1": 36.4, "q3": 39.53}
{"kind": "select1", "delay_ms": 80, "when": "before", "median": 100.41, "q1": 98.3, "q3": 103.72}
(中略)
| Added latency (ms) | Before measurement (ms) | After measurement (ms) |
|---|---|---|
| 0 | 14.83 | 13.97 |
| 20 | 37.68 | 38.20 |
| 80 | 100.41 | 98.79 |
The three conditions below correspond to round-trip times of about 15 ms, about 38 ms, and about 100 ms.
Sequential fetch and batch fetch
The results of running the two implementations 30 times each for each latency setting are as follows. *The reduction rate in the table is the reduction in response time of batch fetch relative to sequential fetch.
$ python latency_bench.py summarize bench.jsonl conn.jsonl
sequential + 0 ms: median 117.2 (q1 109.5, q3 120.6) increase 0.0
sequential +20 ms: median 254.8 (q1 249.7, q3 273.3) increase 137.6
sequential +80 ms: median 625.7 (q1 619.5, q3 632.2) increase 508.5
batched + 0 ms: median 40.5 (q1 37.1, q3 42.6) increase 0.0
batched +20 ms: median 83.2 (q1 81.1, q3 85.9) increase 42.7
batched +80 ms: median 206.8 (q1 204.3, q3 208.6) increase 166.2
(中略)
| Added latency (ms) | Sequential fetch (ms) | Batch fetch (ms) | Reduction rate (%) |
|---|---|---|---|
| 0 | 117.2 | 40.5 | 65.4 |
| 20 | 254.8 | 83.2 | 67.3 |
| 80 | 625.7 | 206.8 | 66.9 |
The time per SQL statement was almost the same in the two implementations.
Batch fetch reduced response time by 65% or more at every latency.
The increase in response time from the 0 ms added-latency case is as follows.
| Added latency (ms) | Increase for sequential fetch (ms) | Increase for batch fetch (ms) |
|---|---|---|
| 20 | 137.6 | 42.7 |
| 80 | 508.5 | 166.2 |
The increase for sequential fetch was about 6.4 to 6.9 times the added latency, and the increase for batch fetch was about 2.1 times.
The increase was close to the added latency multiplied by the number of SQL statements.
How connections are created
The results of comparing the batch fetch process when creating a connection every time versus reusing one are as follows. *The reduction rate in the table is the reduction in response time when reusing a connection relative to creating one every time.
$ python latency_bench.py summarize bench.jsonl conn.jsonl
(中略)
conn new + 0 ms: median 187.3 (q1 178.3, q3 216.5) connect 143.8
conn reuse + 0 ms: median 38.4 (q1 35.1, q3 41.9) connect 0.0
conn new +80 ms: median 721.0 (q1 709.9, q3 732.9) connect 508.8
conn reuse +80 ms: median 210.4 (q1 205.5, q3 222.3) connect 0.0
| Added latency (ms) | Create every time (ms) | Of which connection (ms) | Reuse (ms) | Reduction rate (%) |
|---|---|---|---|---|
| 0 | 187.3 | 143.8 | 38.4 | 79.5 |
| 80 | 721.0 | 508.8 | 210.4 | 70.8 |
When a connection was created every time, the connection time alone took longer than the main processing of the 2 SQL statements.
With 80 ms of added latency, the connection time increased by 365.0 ms. That is an increase of more than 4 times the added latency.
Findings and discussion
From the measurement results, the increase in response time due to communication latency can be estimated as follows.
応答時間の増分 ≒ 往復時間の増分 × 1 リクエストで逐次に行う DB との往復の回数
Since the time per SQL statement was almost the same in the two implementations, it seems that most of the difference in response time in this processing comes from the number of round trips.
The process of creating a connection makes many round trips to the DB … for the TCP connection, the TLS handshake, and authentication. That is probably why the effect of a longer round-trip time was larger than for a single SQL statement. The public endpoint of TiDB Cloud Starter requires TLS connections, so in a design that recreates connections, these round trips occur for every request.
Applying the number of round trips and the round-trip time per request of the app to the estimation formula above, the appropriate measures seem to divide into the following three cases.
| Situation | Example | Effective measure |
|---|---|---|
| Communication latency is unlikely to be a problem | Few round trips to the DB per request and small round-trip time | No action needed |
| Design improvements are effective | N+1, SQL inside loops, a connection per request | Consolidating SQL, connection pooling |
| Consider revisiting placement or configuration | The number of round trips cannot be reduced further, and round-trip time × count exceeds the response time target | Place the app in the same region as the DB, use a private endpoint (AWS PrivateLink) |
For example, with a round-trip time of about 100 ms, 6 round trips to the DB alone push the response time beyond 0.6 seconds. Consolidating them into 2 brought it down to about 0.2 seconds. Improvements that reduce the number of round trips can be tried right away without changing placement, so they are worth considering before revisiting placement.
Summary
I measured response time while varying the round-trip latency between the app and TiDB Cloud Starter and the number of SQL statements per request. The increase in response time is largely determined by the product of the round-trip time and the number of sequential round trips. Consolidating SQL from 6 statements to 2 reduced response time by 65% or more, and reusing connections reduced it by 70% or more. When communication latency is a concern, it seems good to count the number of round trips to the DB per request before changing placement.