# Performance tuning

Source: https://docs.externa.qiick.io/docs/performance-tuning

Operator guide for keeping Public CMS API and admin paths fast: response cache, lean `include`s, queue workers, and Pulse/Health signals.

> **Related**
> [Public CMS API](/docs/public-cms-api) · [Pulse & Health](/docs/pulse-and-health) · [Horizon](/docs/horizon) · [Operations](/docs/operations) · [GA readiness](/docs/ga-readiness)

## Public API response cache

Externa caches successful Public CMS API reads (REST and GraphQL) via `PublicApiResponseCache`.

| Fact         | Detail                                                                                                          |
| ------------ | --------------------------------------------------------------------------------------------------------------- |
| Freshness    | Synchronous collection version bump on write — cache misses immediately after publish/edit                      |
| TTL          | Safety net only (`PublicApiResponseCache::TTL_SECONDS` = **120**) for orphaned payload keys                     |
| Global flush | Settings → **Performance** → flush Public API cache (bumps a global epoch folded into every collection version) |
| Status       | Same Performance page shows cache store, Redis reachability, queue driver, and public API epoch                 |

When debugging stale public reads: confirm the write path bumped the collection, then flush Public API cache from Performance if you force-edited data outside normal writers.

## `include` costs

Public item list/show responses are **lean by default**. Expanding files, users, or relations costs extra queries and payload size.

| Practice                                                | Why                                                          |
| ------------------------------------------------------- | ------------------------------------------------------------ |
| Prefer no `include` for list cards                      | Keep IDs; hydrate detail pages only                          |
| Add `include=files` only when the UI needs file objects | Without it, image/file fields stay as stored IDs             |
| Limit relation field names in `include`                 | Depth 1 only; nested items do not re-apply relation includes |
| Avoid spraying CSV tokens “just in case”                | Unknown tokens are ignored, but known ones still hydrate     |

See [Public CMS API — Expand with `include`](/docs/public-cms-api#expand-with-include).

> **RelationInclude list prewarm**
> Core [#112](https://github.com/qiick-io/externa-core/issues/112) tracks list-level batch prewarm in `RelationIncludeHydrator` to kill N+1 when many items include the same relations. **Not shipped yet** — until it lands, prefer fewer includes on large list pages or fetch detail pages with includes instead of hydrating every row.

## Queues & Horizon

Async work (webhook delivery, zips, imports, AI attachments, Meilisearch / git export jobs) needs a real queue worker. Without it, HTTP stays snappy while background features stall and storage grows.

| Stack        | Worker                                                                                                       |
| ------------ | ------------------------------------------------------------------------------------------------------------ |
| Full (Redis) | `php artisan horizon` + [Horizon](/docs/horizon)                                                             |
| Minimal      | `QUEUE_CONNECTION=database` + `queue:listen` / `queue:work` — [Minimal vs full](/docs/minimal-vs-full-stack) |

Do **not** use `QUEUE_CONNECTION=sync` in production — large zips/imports block requests. Scheduler must run every minute for scheduled publish and cleanup ([Operations](/docs/operations)).

## Reading Pulse for CMS hotspots

Dashboard → **Health** shows Pulse aggregates without embedding Livewire Pulse:

| Signal             | What to check                                              |
| ------------------ | ---------------------------------------------------------- |
| `exceptions_24h`   | Spikes after deploys or bad public clients                 |
| `slow_jobs_24h`    | Webhook / import / zip jobs timing out                     |
| `slow_queries_24h` | Heavy list filters, missing indexes, over-broad `include`s |

Deep dive: `/pulse` (super-admin). Settings → Performance links Jobs / Pulse / Horizon when permitted. Full config: [Pulse & Health](/docs/pulse-and-health).

## Quick checklist

1. Redis (or database queue) + Horizon/worker running in prod
2. Scheduler every minute
3. Public list endpoints without unnecessary `include`
4. Performance flush after unusual cache suspicion
5. Watch Health tab after traffic or content spikes
