Skip to main content

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"}
Accepting only «one» statement is the premise of DDL blocking

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​

SettingDefault
data.gateway.query.max.rows100,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 → the truncated field in the response
  • POST /api/v1/query/stream → the truncated field of the end line

It is also recorded in the server log.

Query result truncated to max_rows=[100000], lang=[PQL]
Truncated results are returned as «success»

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​

SettingDefaultWhat It Is
data.gateway.rate.limit1000Requests per second per client
data.gateway.rate.limit.global0 (unlimited)Combined limit across all clients
data.gateway.trusted.proxiesEmptyList 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 default

The 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​

SettingDefault
data.gateway.max.body.bytes1,048,576 (1 MB)
data.gateway.max.params.bytes65,536 (64 KB)

If exceeded, the gateway rejects early with a 413 — it aborts before reading, not after.

Timeouts and Slow Logging​

SettingDefaultWhat It Is
data.gateway.async.timeout.ms60,000Async request timeout
data.gateway.slow.query.ms10,000Reads slower than this are a warning
data.gateway.slow.update.ms2,000Writes 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]
Why we switched to virtual threads

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​

ItemValue
Tabletm_data_gateway_log (Aster TSDB, timeuuid PK)
What is keptlanguage · query · row count · execution time · success/failure · error message
Writingqueued 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 liveness

Reporting 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 503 LOGIN_NOT_CONFIGURED.
No default account is embedded — that is the intended default

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.