Incident Response

ClickHouse "Too many simultaneous queries" (Error 202): Causes and Fixes

Dashboards start failing with code 202 while the server looks busy but healthy. ClickHouse has hit one of six concurrency limits. Here is how to tell which one, how to see what is occupying the slots, and how to set limits that protect the server without locking you out.

· 12 min read · AG Data Team

The ClickHouse error "Too many simultaneous queries" (code 202, TOO_MANY_SIMULTANEOUS_QUERIES) means the server refused to start a query because a concurrency limit was already reached. Nothing is broken and no data is at risk. But for the dashboard or service that got the error it looks like an outage, and simply raising the limit is often the wrong fix. The useful questions are which limit fired and what is holding the slots.

The usual form of the message:

Code: 202. DB::Exception: Too many simultaneous queries. Maximum: 1000. (TOO_MANY_SIMULTANEOUS_QUERIES)

Six limits, one error code

In ProcessList.cpp, where ClickHouse registers every new query, the same error code is thrown by six different checks. The wording of the message tells you which one you hit:

Too many simultaneous queries. Maximum: N                      -- max_concurrent_queries (server)
Too many simultaneous select queries. Maximum: N, current: M   -- max_concurrent_select_queries
Too many simultaneous insert queries. Maximum: N, current: M   -- max_concurrent_insert_queries
Too many simultaneous queries for all users. Current: M, maximum: N  -- max_concurrent_queries_for_all_users
Too many simultaneous queries for user alice. Current: M, maximum: N -- max_concurrent_queries_for_user
Too many simultaneous waiting queries. Maximum: N, waiting: M        -- max_waiting_queries

All the user-level settings above default to 0 (no limit) in the settings reference.

The useful exception: you can still look

The limits do not apply to everything. ProcessList treats KILL QUERY and a SELECT whose first table is system.processes as unlimited queries, along with internal queries. The source comment calls the check "very rough": USE system; SELECT * FROM processes is not recognized, so always write the table name in full. That means that even when every slot is taken you can see what is running and stop it:

-- works even when the server is at its limit
SELECT user, query_kind, is_initial_query, count() AS queries,
       round(max(elapsed)) AS longest_s
FROM system.processes
GROUP BY user, query_kind, is_initial_query
ORDER BY queries DESC;

Columns from system.processes worth knowing: is_initial_query is 0 for child queries, including those sent to shards for distributed execution; the docs warn that for distributed processing queries are sent to remote servers under the default user, so user on a shard may not be the person who started it; initial_user shows who did. On a cluster, a dashboard query fanned out to several shards also shows up as a running query on each server it touches, so the numbers on shards can be higher than on the node clients talk to.

Find the long ones:

SELECT query_id, user, round(elapsed) AS s,
       formatReadableSize(memory_usage) AS mem, left(query, 120) AS q
FROM system.processes
ORDER BY elapsed DESC
LIMIT 20;

And kill what should not be there:

KILL QUERY WHERE query_id = '6e3c1d8a-...';
KILL QUERY WHERE user = 'bi_dashboard' AND elapsed BETWEEN 300 AND 1e9;

What usually fills the slots

To see when rejections happened and for whom, query system.query_log. Queries that failed before they started are logged with type ExceptionBeforeStart:

SELECT toStartOfMinute(event_time) AS minute, user, count() AS rejected
FROM system.query_log
WHERE type = 'ExceptionBeforeStart' AND exception_code = 202
  AND event_time BETWEEN now() - INTERVAL 1 DAY AND now()
GROUP BY minute, user
ORDER BY minute DESC, rejected DESC
LIMIT 50;

Setting the limits properly

The server-wide limit lives in the server configuration, for example in a file in config.d:

# /etc/clickhouse-server/config.d/concurrency.yaml
max_concurrent_queries: 1000
max_concurrent_select_queries: 0   # 0 = no separate limit
max_concurrent_insert_queries: 0

In the current server code these values are re-applied when the configuration is reloaded, so a restart is not needed to change them. Raising max_concurrent_queries is fine if the server has spare CPU and memory and the queries are short. If the slots are full because queries are slow, a higher limit just lets more slow queries compete for the same CPU.

The more useful tool is the soft limit. The source comment for max_concurrent_queries_for_all_users describes exactly this pattern: set it slightly below the hard limit in the default profile, and to 0 (unlimited) for administrators and accounts that are vital for operations, such as metrics exporters. Then, when the server is overloaded, ordinary users are rejected first and you can still connect and investigate. The SQL form below works for profiles and users managed with SQL; if yours are defined in users.xml or users.d, set the same values there:

-- keep headroom: ordinary users stop at 950 concurrent queries server-wide
ALTER SETTINGS PROFILE default SETTINGS max_concurrent_queries_for_all_users = 950;

-- one noisy dashboard cannot take everything
ALTER USER bi_dashboard SETTINGS max_concurrent_queries_for_user = 50;

-- admins and the metrics exporter are not cut off by the soft limit
ALTER USER dba SETTINGS max_concurrent_queries_for_all_users = 0;

queue_max_wait_ms is another option: when the server-wide limit is reached, a query waits up to this many milliseconds for a free slot instead of failing immediately (default 0, no waiting). A short wait smooths out spikes; a long one only hides the problem and holds client connections open.

Do not "fix" 202 by setting everything to 0. Unlimited concurrency does not make the server faster. Past a certain point, more parallel queries mean more memory pressure, more context switching and slower answers for everybody, which usually ends in "Memory limit exceeded" instead. Keep a hard limit, and use per-user and all-users limits to decide who gets rejected first.

How to avoid the next one

Key takeaways

ClickHouse Concurrency Performance Troubleshooting
Work with us

ClickHouse rejecting queries under load?

We tune ClickHouse clusters for real concurrency: limits, workloads and the queries behind the pile-up. Tell us what you are seeing and we will help you find the bottleneck.

Get in touch More from the blog