Publish Instance Performance Tuning
Series: AEM Architecture & Operations — Part 4

A slow AEM site is rarely fixed by one clever setting. It's usually a handful of ordinary problems: a low cache hit ratio, a query that scans too much, a component doing work on every request, or oversized images. This post gives you a method for finding the real bottleneck, and a checklist of the fixes that matter most, for both AEM 6.5 and AEM as a Cloud Service.
Start with Measurement, Not Guesses
Before changing anything, establish a baseline:
- Response times for key pages, measured from outside (browser developer tools, synthetic monitoring) and inside (request logs)
- Dispatcher/CDN cache hit ratio, because every hit is a request Publish never serves
- Publish load: CPU, memory, thread usage and request rate
- Slow requests: which URLs take the longest, and how often they're requested
Change one thing at a time, and re-measure. Without a baseline you can't tell whether a change helped, hurt or did nothing.
The Performance Layers
Think of a request passing through layers, from cheapest to most expensive:
- Browser cache: free
- CDN: very cheap
- Dispatcher cache: cheap
- Publish rendering: expensive
- Repository and queries: the most expensive part of Publish rendering
The strategy follows directly: push as many requests as possible to the earlier layers, and make whatever reaches Publish cheap.
Layer 1: Maximize Cache Hits
This delivers the largest improvements, so do it first.
- Check what isn't cached and why. Common reasons are query strings, missing file extensions,
no-cacheheaders, authenticated requests and cache rules that deny the path (see Part 1) - Use an allow-list for ignorable query parameters such as
utm_*, so marketing links still hit the cache - Set TTLs with
Cache-Controlheaders so content that changes without activation still expires correctly, and so the CDN can cache effectively - Tune
/statfileslevelso one activation doesn't flush more than it needs to - Keep personalization out of cached HTML. Load user-specific fragments client-side, or use Sling Dynamic Include so the page can be cached while small dynamic pieces are fetched separately
- Use versioned clientlibs and long TTLs for static assets
A site whose pages are mostly served from Dispatcher and the CDN may need only a modest Publish tier, whereas one with a poor hit ratio can overwhelm Publish regardless of tuning.
Layer 2: Make Publish Rendering Cheap
When a request does reach Publish, the cost is mostly your code.
Keep Sling Models Light
- Do only what the page needs. Don't compute things you won't render
- Avoid repository queries inside components that render on every request
- Don't call external services synchronously during rendering. If you must, add timeouts and caching
- Reuse results within a request, rather than recomputing in several places
- Use
@Optionaland lazy getters for expensive values that are only sometimes used
Cache Expensive Results
If a component needs data that's costly to compute and doesn't change often, cache it in an OSGi service with a sensible expiry, and invalidate it deliberately. Be careful that the cache doesn't grow without limit.
Avoid Resource Leaks
A classic cause of gradual slowdown and memory pressure is unclosed resource resolvers:
@Reference
private ResourceResolverFactory resolverFactory;
public String readTitle(String path) {
Map<String, Object> auth = Map.of(ResourceResolverFactory.SUBSERVICE, "my-service");
try (ResourceResolver resolver = resolverFactory.getServiceResourceResolver(auth)) {
Resource resource = resolver.getResource(path);
return resource != null ? resource.getValueMap().get("jcr:title", String.class) : null;
} catch (LoginException e) {
return null;
}
}
Always open service resolvers in a try-with-resources block (or close them in finally), and use service users mapped through a subservice rather than shared admin sessions.
Layer 3: Queries and Indexes
Slow queries are the most frequent cause of slow Publish pages and heavy CPU.
How to Spot Them
- Query Performance (under Tools → Operations → Diagnosis on 6.5) lists recent and slow queries and lets you explain a query to see which index it uses
- Look for traversal warnings in the logs. A query that traverses the repository, rather than using an index, gets slower as content grows
- Check the Index Manager for the indexes in play
How to Fix Them
- Use an index. Add or extend an Oak index definition so the query's properties and node types are covered. On Cloud Service, ship index definitions as code
- Narrow the query. Restrict it by path (
/content/mysite/...), node type and properties, rather than searching the whole repository - Limit results. Add limits and paging
- Avoid wildcards at the start of a value (
LIKE '%term'), which can't use indexes effectively - Don't query at render time at all if you can avoid it. Use a pre-computed property, a tag-based navigation structure or a cache
An example of a well-scoped query:
SELECT * FROM [cq:Page] AS p
WHERE ISDESCENDANTNODE(p, '/content/mysite/en/blog')
AND p.[jcr:content/cq:tags] = 'mysite:topic/aem'
Run it through the explain tool, and confirm it uses an index and not traversal.
Layer 4: Frontend Weight
Backend tuning can't rescue a heavy page.
- Images: serve appropriately sized renditions, modern formats (such as WebP) and responsive
srcsetattributes. The Core Components Image component supports this - Lazy-load images below the fold
- Clientlibs: minify and compress, combine where sensible, and load non-critical JavaScript with
deferorasync - Compression: enable gzip or brotli at the CDN or web server
- Fewer third-party scripts, which are often the biggest slowdown after your own code
Platform-Specific Tuning
AEM 6.5 / AMS (you own the infrastructure)
- JVM: size the heap to the workload, choose a suitable garbage collector and monitor GC pauses rather than guessing
- Repository maintenance: run revision cleanup (online compaction) on a schedule so the segment store doesn't bloat, and plan data store garbage collection if you use a file or cloud data store
- Index maintenance: reindex only when needed, outside peak times
- Instances: add Publish instances behind the load balancer to raise capacity, and keep them identical
- Logging: keep log levels at INFO or above in production, since debug logging slows things down
- Request timing: the request log, and tools such as
rlog, show how long each request took
AEM as a Cloud Service
- Scaling: Publish scales automatically with load, so your priority is making each request cheap, because inefficient code simply costs more compute
- No JVM or OS tuning: that's Adobe's side of the line
- Code quality gates: pay attention to the Cloud Manager performance and quality checks, which flag some problems before they reach production
- Dispatcher and CDN: your configuration is the main lever, so test it on Dev and Stage
- Observability: forward logs to your logging platform and watch response times, errors and cache behavior
- Load testing: run it against Stage and not Production, and check Adobe's current policy and guidance for testing Cloud Service environments before you start
Common Symptoms and Likely Causes
| Symptom | Likely cause | First thing to check |
|---|---|---|
| Fast for some pages, slow for others | Uncached pages or heavy components | Dispatcher cache rules and response headers |
| Slow after every publish | Over-invalidation | /statfileslevel and /invalidate rules |
| CPU high on Publish | Queries without indexes | Query Performance and traversal logs |
| Slow gradually, fixed by restart | Resource or memory leak | Unclosed resolvers, unbounded caches |
| Slow only under load | Insufficient cache hit ratio or capacity | Hit ratio, then instance count |
| Fast server, slow in browser | Frontend weight | Page size, image sizes, third-party scripts |
| Timeouts on certain pages | Slow external calls in rendering | Component code calling remote services |
A Tuning Checklist
- Measure the baseline (response times, hit ratio, load)
- Raise the cache hit ratio (rules, params, TTLs,
/statfileslevel) - Move personalization out of cached HTML
- Find and fix slow queries with Query Performance and indexes
- Remove work from rendering (queries, remote calls, repeated computation)
- Fix resolver leaks and unbounded in-memory caches
- Reduce frontend weight (images, clientlibs, compression, third parties)
- On 6.5, review JVM, compaction and instance count
- Re-measure, and keep the results so you know what worked
- Load-test before major launches, in a non-production environment
Series Wrap-Up
That completes AEM Architecture & Operations. You should now be able to:
- Explain how the Dispatcher cache works and configure invalidation, including
/statfileslevel, TTLs and query parameter rules - Troubleshoot replication on both 6.5 (agents) and Cloud Service (content distribution)
- Describe the differences between AEM 6.5 and AEM as a Cloud Service, and plan a migration
- Find and fix performance bottlenecks on Publish, from caching to queries to frontend weight
The final series, Advanced AEM & Career, covers headless AEM with GraphQL, Edge Delivery Services, architect interview preparation and migration lessons.
About the author
- AEM Technical Lead experienced with building and architecting solutions on AEM Sites and Assets. Sharing practical tutorials, real-world implementation lessons, and best practices to help fellow developers and leads navigate AEM.
Comments
Join the discussionWhat gave you the biggest performance win on Publish: caching, query indexes, lighter components or frontend changes? Share the before and after if you can.
AEM Architecture & Operations
"Dispatcher, replication, performance tuning, and cloud vs on-prem decisions — what it takes to run AEM in production."





