---
name: aem-performance
description: Diagnoses and fixes Adobe Experience Manager performance — CDN and dispatcher cache hit ratio, uncacheable requests, slow JCR queries and Oak indexes, heavy Sling Models and HTL, external API calls, Sling Dynamic Include, Core Web Vitals, and author-side slowness. Use this whenever the user says AEM pages are slow, TTFB or LCP is high, publish CPU spikes, the cache hit ratio is low, logs show "Traversed … nodes" or query limit warnings, needs an Oak index definition or explain query, a Sling Model or HTL render is slow, workflows, replication queues, asset processing or MSM rollouts back up, or asks about thread dumps, request.log, rlog, Developer Console, Lighthouse or load testing AEM. Also apply it when a slow page is really a caching, query or front-end problem rather than server capacity.
metadata:
  technology: AEM
  type: performance
---

# AEM Performance

On AEM, the fastest request is the one publish never sees. Fix cacheability first, then queries, then render code, then the front end — and measure before and after each change with the same tool, because intuition about which layer is slow is usually wrong.

## 1. Triage

| Symptom | Likely cause | Check | Fix |
|---|---|---|---|
| High TTFB on anonymous pages | Cache miss at CDN and dispatcher | `x-cache`/`Age` headers, hit ratio by path | Make the URL cacheable (§2) |
| Publish CPU high under normal traffic | Low hit ratio or frequent invalidation | Requests reaching publish per second; statfile touches | Ignore tracking params, tune statfileslevel, TTLs |
| One page type slow even uncached | Query or render cost | request.log timing, rlog/recent requests, profiler | §3, §4 |
| `Traversed N nodes` / query limit exceptions | Missing or wrong Oak index | Explain query | Add/adjust index, rewrite query (§3) |
| Slow only when a partner API is slow | Synchronous external call without timeout | Thread dump: threads stuck in socket read | Timeouts + cache + fallback (§5) |
| Good TTFB, poor LCP/CLS | Front end: images, render-blocking clientlibs, fonts | Lighthouse / field CWV data | §6 |
| Periodic slowdowns | Background jobs, GC, offloaded workflows on publish | GC logs, Sling Jobs, scheduler activity | Move work to author/offload, fix heap churn |
| Author sluggish, publish fine | Workflow/asset backlog, large folders, rollouts | Workflow console, job queues, replication queue | §7 |

## 2. Cache hit ratio — the first lever

Uncacheable-request causes, most common first:

- **Query strings** (`utm_*`, `gclid`, cache-busters) — ignore them in dispatcher `/ignoreUrlParams` and at the CDN.
- **Cookies/auth headers** on anonymous paths — dispatcher won't cache requests with authorization by default; stop setting session cookies on public pages.
- **`Cache-Control: private/no-store`** emitted by a component or filter for the whole page.
- **Personalised content in HTML** — move it to client-side calls or a Sling Dynamic Include fragment.
- **Selectors/suffixes with unbounded values** — cache-key explosion; validate or restrict them.
- **Over-invalidation** — low `/statfileslevel` means every publish flushes every site.

Target > 90 % CDN hit ratio on anonymous HTML and near 100 % on clientlibs and images (fingerprinted, long TTL).

## 3. Queries and indexes

- Every query in code must hit an index. Run it through the **Explain Query / Query Performance** tool (Developer Console in Cloud Service, Operations → Diagnosis in 6.5) and read the plan: `/* traverse */` or an unexpected index = fix it.
- Traversal warnings in the log and read-limit exceptions (Oak query limits on reads and in-memory results) are errors, not noise.
- Index definitions are code: extend the relevant Lucene index or add a property index, with a versioned `-custom-N` name on Cloud Service. Include `includedPaths`/`queryPaths`, the properties you filter/sort on (`propertyIndex`, `ordered`), and `async` as appropriate.

```
/oak:index/mysite-articles-custom-1
  - type = "lucene", async = ["async","nrt"], compatVersion = 2
  - includedPaths = ["/content/mysite"], queryPaths = ["/content/mysite"]
  - evaluatePathRestrictions = true
  + indexRules/cq:Page/properties/
      + template  (name = "jcr:content/cq:template", propertyIndex = true)
      + published (name = "jcr:content/publishDate", ordered = true)
```

- Constrain queries: node type, path, `p.limit`, `p.guessTotal=true` in QueryBuilder; never compute exact totals for paging over large sets.
- Don't query per render. Precompute lists on activation, cache results in a service, or use resource traversal of a small known subtree.

## 4. Sling Models and HTL

- `@Model(adaptables = SlingHttpServletRequest.class, defaultInjectionStrategy = OPTIONAL)` with `@ValueMapValue`/`@ChildResource` — cheap. Avoid `@Inject` (slow generic injector resolution) in hot components.
- Compute in `@PostConstruct` once; don't call expensive getters repeatedly from HTL loops. Lazy-compute expensive parts only when the template uses them.
- Resolve resources once: pass the `Page`/`Resource` down, don't re-walk `getParent()` chains or re-adapt in every child component.
- Request-scoped cache for data shared by many components on one page (e.g. navigation built once per request in a request attribute or a model via `@Self` adaptation).
- No JCR queries, HTTP calls, or `ResourceResolverFactory.getServiceResourceResolver` per component render.
- Heavy navigation/footers: render once as an Experience Fragment or cached include, not recomputed on every page.

## 5. External calls

- OSGi service with pooled HTTP client, **connect and read timeouts** (seconds, not defaults), circuit breaker/fallback, and a TTL cache (e.g. Caffeine) keyed by request parameters.
- Refresh data in a scheduled job on author or a separate service where freshness allows; publish reads from cache or JCR.
- Prefer client-side fetch for per-user or real-time data so the page stays cacheable.

## 6. Personalised fragments and front end

- **Sling Dynamic Include** replaces a component with an SSI/ESI/client-side include so the page is cached and only the fragment is dynamic. Enable includes in Apache (`mod_include`) for SSI, and make sure the fragment URL is itself cacheable or cheap.
- **LCP**: use the Core Image component with web-optimized image delivery or Dynamic Media (responsive widths, modern formats), don't lazy-load the hero image, preload it if needed, set width/height to avoid CLS.
- **Clientlibs**: split per template, `async`/`defer` non-critical JS, minify, long-cache with fingerprinted URLs; audit the tag manager payload — it is often the largest script.
- **Fonts**: self-host, `font-display: swap`, preload the one or two critical faces, subset.
- Measure with field data (CrUX/RUM) as well as Lighthouse; lab-only scores mislead.

## 7. Author performance

- **Workflows**: use transient workflows where history isn't needed; purge completed instances; avoid launchers firing on every node change.
- **Assets**: Cloud Service processes via asset microservices — keep custom post-processing workflows light; on 6.5 tune DAM Update Asset and consider offloading. Keep folders under a few thousand children.
- **MSM**: roll out in batches off-peak; avoid deep rollout configs triggered on every modification.
- **Replication/distribution queues**: monitor for blocked items; fix the failing item rather than clearing the queue blindly.
- Lucene index reindexing, large package installs and bulk tree activations belong off-peak.

## 8. Cloud Service specifics

- Publish autoscales, but scaling doesn't fix a low hit ratio or a slow query — it just multiplies it.
- No long-running or heavy background jobs on publish; use Sling Jobs on author or external processing.
- Logs: download or stream AEM, dispatcher and CDN logs from Cloud Manager; analyse request.log for slow paths and CDN logs for hit ratio. APM tooling availability depends on the program's entitlements — confirm what's provisioned before relying on it.
- Load-test on stage (inform Adobe support per current policy for large tests), with realistic cache hit ratios and CDN in the path.

## 9. Tools

| Tool | Use for |
|---|---|
| request.log / `rlog.jar` | Slowest URLs and time distribution |
| Recent requests (Felix console, 6.5) / Developer Console (Cloud) | Per-request component timing, OSGi status, queries |
| Explain Query / Query Performance | Index usage and cost |
| Thread dumps (several, 5–10 s apart) | Blocked threads, stuck socket reads, lock contention |
| Heap dumps / GC logs | Memory leaks, unclosed resource resolvers |
| Lighthouse / WebPageTest / RUM | Core Web Vitals, waterfall |
| Load tests (JMeter, Gatling, k6) | Capacity with realistic URL mix and cache behaviour |

## Deliverable format

1. **Symptom and baseline** — metric, value, measurement tool, URL set.
2. **Findings** — table: *layer · evidence · impact · fix · effort*.
3. **Changes** — code/config diffs (index definitions, dispatcher rules, model refactors).
4. **Verification** — same measurement after the change, plus expected hit ratio and CWV impact.
5. **Follow-ups** — monitoring/alerts to prevent regression.

## Anti-patterns to reject

- Adding publish capacity before measuring cache hit ratio.
- Queries in component renders; traversal warnings ignored; `p.limit=-1` with exact totals.
- `@Inject` everywhere; repeated resource resolution; leaked service resource resolvers.
- External HTTP calls without timeouts or caching on the request thread.
- Personalised markup in cached HTML; session cookies on anonymous pages.
- Lazy-loading the LCP image; one giant clientlib on every page; render-blocking tag manager.
- Clearing replication or workflow queues without fixing the root cause.
- Long-running jobs on Cloud Service publish; load tests that bypass the CDN.
