# Performance tuning

Source: https://docs.qiick.io/externa/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](https://docs.qiick.io/externa/public-cms-api) · [Pulse & Health](https://docs.qiick.io/externa/pulse-and-health) · [Horizon](https://docs.qiick.io/externa/horizon) · [Operations](https://docs.qiick.io/externa/operations) · [Environment variables](https://docs.qiick.io/externa/environment-variables) · [GA readiness](https://docs.qiick.io/externa/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 — env `PUBLIC_API_CACHE_TTL` (seconds, default **120**, minimum 1) via `config/api.php` `response_cache_ttl` ([core#115](https://github.com/qiick-io/externa-core/issues/115)). The class constant `TTL_SECONDS = 120` is deprecated; prefer `ttlSeconds()` / config. Global only (no per-collection override yet). |
| 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, public API **epoch**, **TTL seconds**, per-epoch **hits/misses**, and **last flushed** at / by                                                                                                                                                           |

```env
PUBLIC_API_CACHE_TTL=120
```

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. Hit/miss counters reset on flush (new epoch).

## `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`](https://docs.qiick.io/externa/public-cms-api#expand-with-include).

> **RelationInclude list prewarm (1.3.0)**
> Shipped in externa-core **1.3.0** ([core#112](https://github.com/qiick-io/externa-core/issues/112)): Public REST item **lists** call `RelationIncludeHydrator::prewarm` once per response so depth-1 relation targets load in a **flat** query count as parent N grows. Lean `include`s still matter for payload size and GraphQL/detail paths — prefer fewer tokens on large list pages when clients do not need the expanded objects.

## Admin / Public list N+1 (1.3.0)

[core#117](https://github.com/qiick-io/externa-core/issues/117) memoizes `ProjectSettings::raw()` for the request (singleton binding) so admin and Public item lists stop re-querying the `settings` table once per parent row. No operator config — upgrade to 1.3.0+ is enough. If Pulse still shows `slow_queries_24h` spikes on lists, look at filters and over-broad `include`s first.

## 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](https://docs.qiick.io/externa/horizon)                                                             |
| Minimal      | `QUEUE_CONNECTION=database` + `queue:listen` / `queue:work` — [Minimal vs full](https://docs.qiick.io/externa/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](https://docs.qiick.io/externa/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, and shows Public API cache TTL / hits / misses / last flush ([core#115](https://github.com/qiick-io/externa-core/issues/115)). Full config: [Pulse & Health](https://docs.qiick.io/externa/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` (prewarm helps query count; payload still grows)
4. Tune `PUBLIC_API_CACHE_TTL` only if orphaned keys need a different safety-net window
5. Performance flush after unusual cache suspicion; check hits/misses on the same page
6. Watch Health tab after traffic or content spikes
