Skip to main content

Publish Instance Performance Tuning

Series: AEM Architecture & Operations — Part 4

Updated 8 min readMarkdown
Part of seriesAEM Architecture & Operations
Publish Instance Performance Tuning

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:

  1. Browser cache: free
  2. CDN: very cheap
  3. Dispatcher cache: cheap
  4. Publish rendering: expensive
  5. 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-cache headers, 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-Control headers so content that changes without activation still expires correctly, and so the CDN can cache effectively
  • Tune /statfileslevel so 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 @Optional and 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 srcset attributes. 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 defer or async
  • 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

  1. Measure the baseline (response times, hit ratio, load)
  2. Raise the cache hit ratio (rules, params, TTLs, /statfileslevel)
  3. Move personalization out of cached HTML
  4. Find and fix slow queries with Query Performance and indexes
  5. Remove work from rendering (queries, remote calls, repeated computation)
  6. Fix resolver leaks and unbounded in-memory caches
  7. Reduce frontend weight (images, clientlibs, compression, third parties)
  8. On 6.5, review JVM, compaction and instance count
  9. Re-measure, and keep the results so you know what worked
  10. 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.

What 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

Part 4 of 4

"Dispatcher, replication, performance tuning, and cloud vs on-prem decisions — what it takes to run AEM in production."

Start from the beginningDispatcher Caching & InvalidationSeries: AEM Architecture & Operations — Part 1