Control and Limits
A gateway exists not to «open» but to «open without getting hurt». This page records what is blocked, what is limited, and what is logged, along with the values.
Statements That Change the Schema Are Blocked
If the first token is one of the six below, you get a 400 QUERY_SYNTAX.
DROP · TRUNCATE · ALTER · CREATE · GRANT · REVOKE
{"result":"ERROR","error_code":"QUERY_SYNTAX",
"message":"DDL statements are not allowed: DROP"}
Comments cannot be used to bypass this. Block and line comments are stripped first, and then the first token is checked.
/* 안녕 */ DROP TABLE pp.mm_tag -- 막힙니다
-- 한 줄
DROP TABLE pp.mm_tag -- 막힙니다
If nothing remains after stripping comments, that is also a 400.
Semicolons Cause Rejection
{"result":"ERROR","error_code":"QUERY_SYNTAX",
"message":"Multiple statements are not allowed"}
Inspecting only the first token only works when there is exactly one statement per request. If SELECT 1; DROP TABLE x were allowed, the first token would be SELECT and the check would pass as-is. Therefore, if a semicolon is visible, the request is rejected before looking at the contents.
Result Row Limit
| Setting | Default |
|---|---|
data.gateway.query.max.rows | 100,000 (0 = unlimited) |
When the limit is reached, the gateway stops reading right there — it does not read the entire result and then truncate it. This way, even huge results do not create an infinite stream.
POST /api/v1/query→ thetruncatedfield in the responsePOST /api/v1/query/stream→ thetruncatedfield of theendline
It is also recorded in the server log.
Query result truncated to max_rows=[100000], lang=[PQL]
The HTTP status is 200 and the rows look normal. If you do not check truncated, you could mistake a truncated result for the complete one and calculate based on it. That is why this flag is always carried.
Rate Limiting
| Setting | Default | What It Is |
|---|---|---|
data.gateway.rate.limit | 1000 | Requests per second per client |
data.gateway.rate.limit.global | 0 (unlimited) | Combined limit across all clients |
data.gateway.trusted.proxies | Empty | List of direct peers that can be trusted for X-Forwarded-For |
The bucket key is the trusted client IP.
X-Forwarded-For is ignored by defaultThe first hop of XFF is only trusted as the client IP when the direct peer is listed in trusted.proxies. If left empty, the actual connection address is always used — otherwise, a single header could be forged to bypass the limit.
If you are behind a reverse proxy, you must add that proxy's address to this list. If you do not, all requests will pile into one bucket for that proxy.
The global limit is a defense against «bucket flooding» — no matter how many different IPs you create, the total throughput cannot exceed this value.
/admin/* also «shares» the same limit/api/* and /admin/* use the same counter. If they were separate, the global limit would effectively double, and you could divide the load between the two entry points to bypass it.
Request Size
| Setting | Default |
|---|---|
data.gateway.max.body.bytes | 1,048,576 (1 MB) |
data.gateway.max.params.bytes | 65,536 (64 KB) |
If exceeded, the gateway rejects early with a 413 — it aborts before reading, not after.
Timeouts and Slow Logging
| Setting | Default | What It Is |
|---|---|---|
data.gateway.async.timeout.ms | 60,000 | Async request timeout |
data.gateway.slow.query.ms | 10,000 | Reads slower than this are a warning |
data.gateway.slow.update.ms | 2,000 | Writes slower than this are a warning |
Requests are handled on virtual threads. The concurrency limit is data.gateway.async.concurrency.limit, and the default is 256 (unlimited if 0). When the limit is reached, the submitter waits for a slot to free up — nothing is dropped, and backpressure is applied. The startup log prints the actual values.
Async executor: virtual threads, concurrency limit=[256]
The previous fixed pool did not spawn more than 20 threads until the queue had 500 pending items, so the effective concurrent queries were 20, and requests waiting in queue would hit the 60-second timeout and return 503. Since the real bottleneck was the DB connection pool, we now only have a single concurrency limit instead of tuning the pool size.
Query Log — Every Request Is Logged
| Item | Value |
|---|---|
| Table | tm_data_gateway_log (Aster TSDB, timeuuid PK) |
| What is kept | language · query · row count · execution time · success/failure · error message |
| Writing | queued and flushed in batches — does not block the query path |
The queue's status is exposed by the gateway.querylog.* gauge of GET /admin/metrics.
/api/health is readiness, not livenessReporting UP just because the context is alive is meaningless — it is UP only when ready to receive queries. The check set (database · inmemory · query_log) must all be true; otherwise, it is STARTING or DEGRADED. A gateway whose query log is not initialized is not UP — if it appears «normal» without audit records, you won't be able to answer «who queried what» later.
Authentication · Console Accounts
- Authentication is determined by a single Spring Security chain. Any path not in the allowlist returns 401, not anonymous.
- No sessions or cookies are created (STATELESS). Credentials come in the header with each request.
- The console login account (
data.gateway.web.user·data.gateway.web.password) is only enabled when both are filled in. If either is empty, the screen says «not set up» and everything is rejected with a 503LOGIN_NOT_CONFIGURED.
Embedding a default account ships the same credentials to every customer installation, and nobody changes them. Put the values in the rendered configuration (the template in plantpulse-datalake-cli).
The ID and password are always both checked (even if one is wrong, the other is not skipped), and the failure response does not say which one was wrong.
To Change the Values
The source of truth is config/templates/ of plantpulse-datalake-cli. Changes made inside the container disappear on restart.
docker exec plantpulse-datalake pd config diff --templates
docker exec plantpulse-datalake pd restart data-gateway
For the procedure, see How to Change Settings; for key rotation, see Passwords and API Keys.