# Staff SiteSerializer category\_types performs one DB query per SiteSetting in configuration schemas

**URL:** <https://meta.discourse.org/t/staff-siteserializer-category-types-performs-one-db-query-per-sitesetting-in-configuration-schemas/410304>\
**Category:** Development\
**Tags:** performance, development\
**Created:** [August 18, 2026, 8:48am UTC](https://meta.discourse.org/t/staff-siteserializer-category-types-performs-one-db-query-per-sitesetting-in-configuration-schemas/410304 "2026-08-18T08:48:19Z")\
**Posts on this page:** 2\
**Page:** 1

<div class="post-metadata">

**Author:** ![Ethsim2](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/ethsim2/32/522255_2.png) [@Ethsim2](https://meta.discourse.org/u/Ethsim2)\
**Post date:** [August 18, 2026, 8:48am UTC](https://meta.discourse.org/t/staff-siteserializer-category-types-performs-one-db-query-per-sitesetting-in-configuration-schemas/410304/1 "2026-08-18T08:48:20Z")

</div>

I noticed what looks like an N-per-setting query pattern while investigating initial page-load performance for staff users.

On my site, an ordinary non-staff account loads `/latest` noticeably faster than my admin/staff account. While profiling the staff request with Rack Mini Profiler, I found that `SiteSerializer#category_types` appears to build category-type configuration metadata by checking each SiteSetting individually to determine whether it has been overridden.

The relevant path appears to be:

`SiteSerializer#category_types`  
→ `Categories::TypeRegistry.list`  
→ category type `metadata`  
→ `resolved_configuration_schema`  
→ `site_setting_overridden?`  
→ `SiteSetting.provider.find`

Current code:

> <https://github.com/discourse/discourse/blob/68781f669f8fa70395b816a62689b09d229811dd/app/services/categories/types/base.rb#L422-L457>

In particular, `site_setting_overridden?` calls:

```plaintext
SiteSetting.provider.find(setting_name.to_sym).present?

```

and `resolved_configuration_schema` calls that while iterating the SiteSettings in the configuration schema.

`SiteSettings::DbProvider#find` then performs an individual query for the named setting:

> <https://github.com/discourse/discourse/blob/68781f669f8fa70395b816a62689b09d229811dd/lib/site_settings/db_provider.rb#L13-L24>

In the profiler output I saw individual queries such as:

```plaintext
SELECT name, data_type, value
FROM site_settings
WHERE name = 'discourse_post_event_allowed_on_groups';

SELECT name, data_type, value
FROM site_settings
WHERE name = 'use_local_event_date';

SELECT name, data_type, value
FROM site_settings
WHERE name = 'show_filter_by_solved_status';

SELECT name, data_type, value
FROM site_settings
WHERE name = 'topic_voting_show_who_voted';

```

There were 16 queries matching this `site_settings WHERE name = ...` pattern in the captured request. These were for different setting names, so I am not suggesting that the same lookup is simply being repeated; rather, it appears to be one DB lookup per SiteSetting represented in the category-type configuration schemas.

This seems particularly relevant to staff users because `category_types` is only included for staff:

> <https://github.com/discourse/discourse/blob/68781f669f8fa70395b816a62689b09d229811dd/app/serializers/site_serializer.rb#L475-L481>

My profiling also showed plugin-provided category configuration participating in this path, including Discourse Solved, calendar/events-related settings and Topic Voting settings.

For comparison, on the same Discourse installation and connection, a warm `/latest` load was approximately:

```plaintext
Admin/staff account:
DOMContentLoaded: roughly 0.77–1.03 s
Initial HTML: roughly 2.08 MB decoded

Ordinary account:
DOMContentLoaded: roughly 0.51 s
Total decoded resources: roughly 1.50 MB

```

The ordinary-account PWA also ended up loading at least as quickly as the AMD Developer Community PWA in subsequent testing, so this does not appear to be a general origin/CDN performance issue.

I also had one unusual staff [Safe Mode](https://meta.discourse.org/t/53504?silent=true) request where `layouts/application` produced 1,362 SQL queries and took about 2.9 seconds. I do **not** think the per-SiteSetting queries above account for all of those queries, so I am treating that separately rather than claiming this is responsible for the entire outlier.

Would it make sense for the overridden-setting state used by `resolved_configuration_schema` to be fetched/batched once, rather than calling `SiteSetting.provider.find` separately for each SiteSetting?

For example, `DbProvider` already has an `all` method, although I am not suggesting that using `all` directly is necessarily the correct implementation - mainly wondering whether avoiding the N-per-setting database lookups here would be worthwhile.

I checked the same path at current `main` and it appears that the per-setting `provider.find` behaviour is still present.

---

<div class="post-metadata">

**Author:** ![Ethsim2](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/ethsim2/32/522255_2.png) [@Ethsim2](https://meta.discourse.org/u/Ethsim2)\
**Post date:** [August 21, 2026, 11:15am UTC](https://meta.discourse.org/t/staff-siteserializer-category-types-performs-one-db-query-per-sitesetting-in-configuration-schemas/410304/3 "2026-08-21T11:15:24Z")

</div>

I’ve now reproduced this against current `main` and opened a PR which batches these SiteSetting override lookups:

> <https://github.com/discourse/discourse/pull/42787>
>
> This avoids performing a separate SiteSetting provider lookup for each SiteSetti…ng represented in category type configuration schemas.
> 
> Previously, \`resolved\_configuration\_schema\` called \`SiteSetting.provider.find\` for each configured site setting. When \`Categories::TypeRegistry.list\` serialized multiple category types, this resulted in multiple individual \`site\_settings WHERE name = ?\` queries.
> 
> This change:
> 
> \- loads persisted SiteSetting overrides once in \`TypeRegistry.list\`
> \- reuses that override information across all serialized category types
> \- retains a single bulk-provider fallback for direct metadata/schema resolution
> \- preserves the existing \`overridden\` behavior
> 
> While profiling a staff site payload, I observed 16 individual SiteSetting lookup queries originating from this path.
> 
> Tests cover both multiple SiteSettings within one category type and multiple category types sharing the same bulk lookup.
> 
> Local verification:
> 
> \- RuboCop: no offenses
> \- Syntax Tree: expected format
> \- relevant category/serializer specs: 149 examples, 0 failures

While working through it, I found there were effectively two levels to the query pattern.

The original implementation called:

```plaintext
SiteSetting.provider.find(...)

```

once for each SiteSetting represented in a category type’s configuration schema.

A first batching approach could reduce that to one `provider.all` per category type, but `Categories::TypeRegistry.list` calls `metadata` separately for each registered type, so that would still result in multiple bulk queries when several category types provide SiteSettings.

The PR instead loads the persisted SiteSetting overrides once in `TypeRegistry.list` and passes that information through to all of the category types being serialized. Direct metadata/schema resolution retains a single bulk-provider fallback.

So for the staff site payload path I originally reported, the intended shape changes from approximately:

```plaintext
one SELECT ... WHERE name = ? per configured SiteSetting

```

to:

```plaintext
one bulk SELECT from site_settings for TypeRegistry.list

```

The existing overridden/not-overridden behaviour is preserved.

I added regression coverage for both cases:

- multiple SiteSettings within a single category type;

- multiple category types sharing the same bulk override lookup.

Locally, the relevant category and serializer specs passed:

```plaintext
149 examples, 0 failures

```

The PR has also now completed upstream CI successfully: **14 successful checks, 0 failures, 0 pending** (with 2 checks skipped).

So this appears to address the specific N-per-setting behaviour from the profiler capture in the opening post, rather than the separate much larger SQL-query outlier I mentioned near the end of the opening post.
