Confluence heap size for a Cloud migration: Data Center memory settings, Synchrony, CCMA
Mihai Perdum
Author
34 min readSeptember 17, 2026
Key takeaways
END STATE: a Confluence Data Center 10.2 cluster whose heap is calculated from your own GC log and Atlassian's node splits, whose setenv.sh, server.xml, confluence.cfg.xml and synchrony-args.properties carry only shipped or documented values, and whose nodes, pool, open files and read-only mode are set for the Cloud Migration Assistant.
Atlassian publishes no recommended Confluence heap. It publishes a floor for the migration assistant (4 GB), the heaps it tested with (8 GB on 32 GB nodes at 102,000 users; 12 GB in its own performance toolkit), node splits (3 GB of 10, 8 GB of 16), a GC-log shape from its Jira knowledge base (about 40% of the heap live after collection), and a forbidden band: never 32 to 47 GB, go to 48 or more. That is enough to calculate from.
The assistant's shipped code protects itself from your heap: with its memory budget enabled it will not start an attachment step above 80% heap use, pauses above 90%, and aborts above 95%. Its parallelism comes from CPU cores and node count, not from any property you can set; Atlassian documents no CCMA system property at all.
Synchrony is its own JVM: 2g by default, 4g only if it runs out of memory in a cluster, set through -Dsynchrony.memory.max; everything else through synchrony-args.properties in the shared home. Atlassian itself runs Synchrony on two 2-vCPU nodes for 2,400 employees.
Before the run: open files near 32,768, the native-thread OutOfMemoryError fix if you see it, disk under <local-home>/temp for the export, read-only mode on (it does not stop database writes), attachments first, users first above 2,000 accounts, 400,000 pages a day as the median rate.
What 'massive' means in Atlassian's tiers: 10,000 to 100,000 local users is Large, 100,000 to 250,000 is XLarge. The largest Confluence Atlassian has published a test for is 102,000 users and a million HTTP calls an hour, on an 8 GB heap and four 16-core nodes; its own instance serves 2,400 people at 277 peak concurrent on four 8-core nodes with a 60-second Hazelcast heartbeat.
The Confluence heap size, the Data Center memory settings and the JVM parameters that matter before a Cloud migration are fewer than the forum threads suggest, and Atlassian has written down nearly all of them, in about fifty pages that were never meant to be read together. The migration adds one hard requirement, one mechanism most administrators have never seen, and a set of Cloud limits that fail a large migration more often than memory does. So this tutorial takes the setenv.sh that Confluence 10.2.18 ships, reads it line by line against what Atlassian says about each line, adds the layer Atlassian documents for the Confluence Cloud Migration Assistant, and gives you a way to calculate your own heap instead of borrowing a number.
The rule for every value is the one I would hold a client cluster to: shipped by Atlassian, documented by Atlassian, or defined in the JDK 21 reference, or it does not go in. Where Atlassian gives a rule and no number, you get the rule and a method. Where two Atlassian pages disagree, you get both. Where the only source is the assistant's own shipped code, it is labelled as code, not as a setting to change. Where I measured something myself, it is labelled as measured on my machine and not on a Confluence node. Every page cited was fetched on 14 September 2026.
Jira Data Center has a different setenv.sh, a different migration assistant with a published heap table, and gets its own tutorial.
Note
Prerequisites
Confluence Data Center 10.2 on Linux, with access to <install-directory>/bin, <install-directory>/conf, the local home and the shared home. Windows uses setenv.bat with the same values.
Java 21. Confluence 10.2's supported platforms page lists Oracle and Temurin Java 21 and nothing else, and the installer bundles Temurin 21.0.4.7. The startup script only refuses versions below 17, which is looser than the support statement; follow the support statement.
The Confluence Cloud Migration Assistant. 10.2.18 bundles 3.13.21; the Marketplace's current release is 3.14.1 (8 September 2026), compatible with Data Center 8.0.0 through 10.2.18. Update it: Atlassian's own system check says "The Migration Assistant is not up to date ... Update to the latest supported version."
Your instance's numbers, from Administration, General Configuration, System Information, scrolled to Confluence Usage: content in all versions, total spaces, local users; and from your monitoring, HTTP calls per hour. Atlassian's load profiles are keyed to those four.
Ten minutes of GC logs from production at peak, and a staging clone with the production Server ID restored after cloning. Both are inputs to step 2.
Support and a partner engaged early if you are large: "If you'll be performing your migration over a weekend or holiday, or will have over 1,000 users in cloud, we recommend getting in touch with our support team at least two weeks in advance", and "For all teams with over 1000 users, we strongly recommend working with a Cloud specialized partner".
1
Read the setenv.sh you were shipped before you change anything in it, and know which four blocks it has.
2
Calculate the heap from the migration floor, Atlassian's node splits and tested heaps, your GC log, and the 32 to 47 GB rule.
3
Leave G1 alone, read the log it already writes, and decide about pre-touch with the measurements in front of you.
4
Move to the large-instance lines the file already contains
384 MB of code cache and no upgrade recovery file.
5
Give Synchrony its own memory through the property Confluence owns, and its other flags through the shared-home args file.
6
Pair the Tomcat thread pool with the database pool in the ratio Confluence checks at startup.
7
Prepare the cluster for the migration assistant
the floor, open files, disk, nodes, read-only mode, the order of operations, and what its code does with your heap.
8
Enforce the cluster rules and read what Atlassian has tested at extra-large scale before you decide on nodes.
9
Verify it took from the logs, System Information and the health checks, on every node, and measure the test migration.
Step 1 — Read the Confluence setenv.sh you were shipped
Download the 10.2.18 tar.gz from Atlassian, check its md5 against the download feed (1a7e53b1cddeb4c5da5eed6bdc200298 for 10.2.18, released 8 September 2026), and extract just the script:
The file has four blocks, and Atlassian's migration guide for setenv names them: Java heap size; recommended properties for large instances; additional Confluence system properties; and required properties "that you should not change, unless directed to do so by our support team". The comment at the top of the editable section says that this section holds the commonly modified options, that you copy it to reapply your customisations when upgrading, and that you always review the new file for changes to the defaults.
These are the lines that carry values, as shipped in 10.2.18:
bash
1# line 83 — the heap2CATALINA_OPTS="-Xms1024m -Xmx1024m ${CATALINA_OPTS}"34# lines 85-86 — "Default values for small to medium size instances"5CATALINA_OPTS="-XX:ReservedCodeCacheSize=256m ${CATALINA_OPTS}"67# line 89 — the JPMS arguments Confluence needs on Java 17+; never edit8CATALINA_OPTS="@$CATALINA_HOME/confluence/WEB-INF/jpms-args.txt ${CATALINA_OPTS}"910# lines 92-94 — the garbage collector, overridable through CONFLUENCE_GC_OPTS11if[-z"${CONFLUENCE_GC_OPTS}"];then12CONFLUENCE_GC_OPTS="-XX:G1ReservePercent=20 -XX:+UseG1GC -XX:+ExplicitGCInvokesConcurrent"13fi1415# lines 97-99 — GC logging, on by default, overridable through CONFLUENCE_GC_LOG16CONFLUENCE_GC_LOG="-Xlog:gc*:file=$LOGBASEABS/logs/gc-%t.log:tags,time,uptime,level:filecount=5,filesize=2M"1718# lines 101-108 — "Recommended values for medium to large, and enterprise size instances", commented out19#CATALINA_OPTS="-XX:ReservedCodeCacheSize=384m ${CATALINA_OPTS}"20#CATALINA_OPTS="-Dconfluence.upgrade.recovery.file.enabled=false ${CATALINA_OPTS}"21#CATALINA_OPTS="-Xlog:gc*=debug:file=$LOGBASEABS/logs/gc-%t.log:tags,time,uptime,level:filecount=5,filesize=2M ${CATALINA_OPTS}"
Below line 122 sits the block headed "You should not need to change the options below unless recommended by Atlassian Support": -XX:+IgnoreUnrecognizedVMOptions, the two GC variables, -Djava.awt.headless=true, -Datlassian.plugins.enable.wait=300, -Dsynchrony.enable.xhr.fallback=true, the context path, a websocket buffer size, and a final ${START_CONFLUENCE_JAVA_OPTS} hook. Three things follow.
First, the file already knows about large instances. The tuning Atlassian is willing to put its name to is the commented block at lines 101 to 108, and step 4 is nothing more than uncommenting it.
Second, the override hooks are environment variables named CONFLUENCE_GC_OPTS, CONFLUENCE_GC_LOG and START_CONFLUENCE_JAVA_OPTS. If you have seen JVM_SUPPORT_RECOMMENDED_ARGS in a runbook, that is the Docker image's variable, alongside JVM_MINIMUM_MEMORY, JVM_MAXIMUM_MEMORY and JVM_RESERVED_CODE_CACHE_SIZE. It does not appear in the shipped setenv.sh at all; grep returns zero.
Third, the file is version-specific, and Atlassian is explicit about not carrying an old one forward: "Rather than copying over your existing setenv.sh or setenv.bat file, we strongly recommend following the process outlined on this page to incorporate your previous configuration into the new file." The page also names one known issue, CONFSERVER-61415, Java 11 garbage collection logging settings not correctly applied on start, fixed in Confluence 8.0.0.
How you know it worked: you can name, for every uncommented line in your production setenv.sh, which of the four blocks it belongs to and which Atlassian page or comment justifies it. Anything you cannot place is a candidate for removal, and Atlassian's garbage-collector page says exactly that: "If you have added JVM parameters in the past and are experiencing difficulties with GC now, we'd recommend you remove all GC related parameters, unless you added them to solve a specific problem, and they did in fact solve that problem."
Step 2 — Calculate the Confluence heap size (and skip 32 to 47 GB)
There is no Atlassian-recommended maximum heap for a large Confluence Data Center, and Atlassian says so: "We aren't able to give a concrete recommendation for the amount of memory to allocate because that will depend greatly on your server setup, the size of your user base, and their behavior." What Atlassian publishes is five things that together give you a calculation: a floor from the migration assistant, the node splits it uses as worked examples, the heaps it has tested and runs, a shape rule for the GC log, and one forbidden band. Do them in order and write each result down; the heap is the largest of the first four that does not land in the fifth.
Input one: the migration floor. The Confluence pre-migration checklist: "Depending on the amount of data to be migrated, Confluence Data Center might experience an OutOfMemory error. This will crash the entire migration. To prevent this, ensure that your application is running with at least 4GB of Heap Allocation (if not, make it as close as possible)." Its check is on setenv.sh: "confirm both parameters below are greater than 4096m or 4g ... -Xms4096m -Xmx4096m", or in a support zip, grep '<max-heap>' application.xml. The same page labels that item "Recommended" in its table and "[mandatory]" in its heading; both wordings are on the page today, and a migration that crashes on OutOfMemoryError makes the second the one to believe. That is the only heap number Atlassian gives for the Confluence assistant. There is no Confluence table of heap against pages or users; the one Atlassian migration heap table that exists is Jira's, keyed to issues and CPU cores, and it does not apply here.
Input two: the node splits. The clustering page gives two worked examples of how a node's RAM divides, and says the default must go up: "The maximum heap (-Xmx) for the Confluence application is set in the setenv.sh or setenv.bat file. The default should be increased for Data Center. We recommend keeping the minimum (Xms) and maximum (Xmx) heap the same value."
node RAM
operating system
Confluence JVM
external process pool
Synchrony
10 GB
2 GB
4 GB, -Xmx 3GB
2 GB, two sandboxes at 512 MB
2 GB
16 GB
2 GB
10 GB, -Xmx 8GB
2 GB, two sandboxes at 512 MB
2 GB
3 rows × 5 columnsHeader row enabled
Read the table for its shape: the JVM is one of four consumers, Synchrony and the sandbox pool have their own memory, the operating system keeps 2 GB, and the heap is the JVM's allowance minus its own off-heap needs. The page adds that "We recommend that each Confluence node has a minimum of 10GB of RAM. A high number of concurrent users means that a lot of RAM will be consumed."
Input three: the heaps Atlassian has tested and runs. Atlassian publishes no recommendation, but it publishes what it used. The extra-large AWS benchmark, the largest Confluence Atlassian has ever published a test for, ran a dataset of 34.9 million content items, 10,500 spaces, 102,000 local users, 1,200 active users and a million HTTP calls an hour, on three or four c5.4xlarge nodes of 16 CPUs and 32 GB each, and its configuration section says: "When testing c5.xlarge (which only has 8GB of RAM), we used 4GB for JVM heap. For all others, we used 8GB." Eight gigabytes of heap on a 32 GB node, for the biggest Confluence in Atlassian's published record, on Confluence 6.13 and 6.15. Atlassian's Data Center App Performance Toolkit, the bench it publishes for app vendors' "large" performance tests, ships confluence_max_heap = "12288m" in a 16 GiB container with Synchrony in its own 3 GiB container, for a dataset of about 900,000 pages, 2.3 million attachments and 5,000 users on Confluence 10.2.2. The 2016 lab test used "8GB Xmx settings per node" in one sentence and "6 GB of memory was allocated to the JVM" in another, on the same page. Atlassian's own production instance, 14.9 million content items for 2,400 employees, states no heap anywhere. So the largest Confluence heap in any Atlassian-authored artefact is 12 GB, and every one of those figures is a test configuration, not a recommendation.
Input four: your GC log. Atlassian's Confluence GC page sets the direction: "Keep your heap as small as possible, without the instance experiencing OutOfMemory errors ... Do not increase the heap further than required, as this will result in longer garbage collections", and it adds that "We have also observed that G1GC performs better with a larger heap (2gb)." The shape rule comes from a Jira knowledge base article, and I label it as Jira's: "The memory footprint area (max and min of the saw-toothed like lines) should be around 40% of your total Java heap size", "The Young generation size should not exceed more than half the total Java heap size", and "A full GC should not take longer than 3 to 5 seconds." The method: take ten minutes of GC logs at peak, read the heap occupancy after each young collection and after any concurrent cycle, and that live number divided by about 0.4 is the heap your steady state wants. Step 3 shows what those lines look like. The migration assistant's own code gives a second constraint on the same number, and step 7 explains it: with its memory budget enabled, it will not start an attachment step while heap use is above 80% of the maximum, so the live set plus the export's working memory has to sit under 80% of whatever you choose.
Input five: the forbidden band. Atlassian's Confluence knowledge base article of April 2025 is the one page that contemplates a heap above 32 GB, and it gives a rule, not an invitation: "Java disables Compressed Oops for applications heaps larger than 32 GB. This changes the memory allocation size from 4 to 8 bytes, reducing the number of objects it can store in the heap. Therefore, increasing the maximum heap to a value near 32 GB and up to 47 GB will actually decrease the amount of memory available, leading to possible Java OutOfMemoryErrors ... If you need to increase the Confluence memory heap size to near or higher than 32 GB, please set it to a value greater or equal to 48 GB and add further memory from that point." The GC overhead page repeats it: "Avoid heap sizes between 32GB and 47GB due to Compressed OOPs limitations." The JDK reference gives the mechanism: compressed pointers cover a range that "By default ... is 32 GB", and on JDK 21 the switch happens exactly at the number:
Then the two rules that shape the increments. Grow in small steps on evidence: the OutOfMemory page says "Increase Xmx in small increments (1-2GB at a time), until you no longer experience the OOM error", the older memory page says "512mb at a time", the GC page says "512mb or 1gb allotments"; three pages, three increments, one shape. And never a half gigabyte: "When setting the value in gigabytes, do not use .5 increments as this will cause the JVM to not start properly", which on JDK 21 looks like this:
bash
1java-Xmx2.5g-version
text
1Invalid maximum heap size: -Xmx2.5g
2Error: Could not create the Java Virtual Machine.
Write -Xmx2560m if you mean two and a half gigabytes.
The calculation, worked for the instance this tutorial is for. Say 25,000 local users, 12 million content items in all versions, 4,000 spaces, 600,000 HTTP calls an hour at peak, on 16-core nodes with 32 GB of RAM. Atlassian's load profiles put the users in Large (10,000 to 100,000), the content in XLarge (10 to 25 million), the spaces in Large, the traffic in Large (350,000 to 700,000), and the page says to read the highest: "your site might be Extra Large for traffic and only Medium for the amount of content, or vice versa." Input one says 4 GB is the floor for the migration. Input two's 16 GB row gives 8 GB of heap inside a 10 GB JVM allowance; on a 32 GB node the same split leaves 26 GB after the operating system, 2 GB of sandboxes and 2 GB of Synchrony, of which the JVM's off-heap memory takes a share. Input three says Atlassian ran its 102,000-user test on 8 GB and its current toolkit on 12 GB. Input four: if the GC log at peak shows 5 GB live after collection, 5 divided by 0.4 is about 12 GB, and 80% of 12 GB is 9.6 GB, so the export has 4.6 GB of headroom above the live set before the assistant pauses itself. Input five is not in play at these sizes. The heap is 12 GB, and the honest description of it is that it matches Atlassian's own current test bench and sits inside Atlassian's own node split. If the live set were 9 GB, the arithmetic would say 22 to 24 GB; with the split's 2 GB of JVM off-heap, 2 for the operating system, 2 for sandboxes and 2 for Synchrony that is 30 to 32 GB, the whole node with nothing spare, so the node moves up a size; if the arithmetic ever says 30 to 40, the band forbids it and the answers are 31 or a node with room for 48.
bash
1CATALINA_OPTS="-Xms12g -Xmx12g ${CATALINA_OPTS}"
One more source of OutOfMemoryError is not a heap-size problem at all. "Memory usage is most likely to be maximized under peak load, and when creating a site XML backup. In many cases, the backup can be the cause of the OOM", and Atlassian's answer is not more heap: "Atlassian recommends in normal use, to disable the XML backup and use a Production Backup Strategy." Check the scheduled jobs before you grow the heap for a nightly spike.
How you know it worked: two places. Atlassian's OutOfMemory page: "To verify what settings are in place after restarting, check atlassian-confluence.log for the last startup and search for "-Xms" or "-Xmx"." And the UI: Administration, General Configuration, System Information, scroll to System Properties, where the expanded CATALINA_OPTS are printed. The JVM itself can also tell you before Confluence starts. On JDK 21, with the shipped G1 flags and the step-4 code cache:
That output is from OpenJDK 21.0.12.1 on a Mac, not from a Confluence node, but the flags are the same flags and the right-hand column is the point: {command line} means your value took, {default} and {ergonomic} mean the JVM chose. Every flag you set should read {command line}.
Step 3 — Leave G1 alone, read the log it already writes, and decide about pre-touch
Confluence ships G1 and Atlassian recommends it: "Confluence uses the garbage first garbage collector (G1GC) by default. This is the garbage collector we recommend." The same page adds that "G1GC performs better with a larger heap (2gb)", and the GC overhead page lists "Adjust for large heaps (>4GB); Fine-tune based on usage patterns" without naming a single flag to adjust. That silence is why this tutorial adds none. The shipped flags are -XX:+UseG1GC -XX:G1ReservePercent=20 -XX:+ExplicitGCInvokesConcurrent, and no Atlassian page explains the second and third. They are what Atlassian ships; that is the whole justification, and it is enough. The JDK 21 reference does define the third: "Enables invoking of concurrent GC by using the System.gc() request. This option is disabled by default and can be enabled only with the -XX:+UseG1GC option."
What Atlassian warns against is more useful than what it recommends. "Don't use the Concurrent Mark Sweep (CMS) Collector with Confluence, unless advised by Atlassian Support. It requires extensive manual tuning and testing, and is likely to result in degraded performance." On JDK 21 the question does not arise:
bash
1java-XX:+UseConcMarkSweepGC-version
text
1Unrecognized VM option 'UseConcMarkSweepGC'
2Error: Could not create the Java Virtual Machine.
If that flag is in a setenv.sh you inherited, the only reason Confluence starts is the shipped -XX:+IgnoreUnrecognizedVMOptions, which is exactly why you should grep your file for options the JDK no longer knows. And a Jira knowledge base article states the collector list for both products: "Jira and Confluence only support parallel GC or G1GC." That sentence is why this tutorial does not discuss ZGC, which JDK 21 documents as available and Atlassian mentions nowhere.
The older enterprise GC tuning guide carries its own warning label: "This guide was originally prepared for JIRA 6.x. Many of the concepts are still relevant, but several of the examples are now out of date." Take its principles and none of its flags: "Pick two of the three performance goals" of latency, throughput and footprint, and "by supplying explicit tunings you may actually degrade your performance." That is why this step does not set -XX:MaxGCPauseMillis, -XX:ParallelGCThreads, -XX:ConcGCThreads, -XX:G1HeapRegionSize or string deduplication. No Atlassian page for Confluence sets any of them; the JDK describes the pause target as "a soft goal ... By default, for G1 the maximum pause time target is 200 milliseconds", and the PrintFlagsFinal output above shows it at 200 {default}. Leave it there.
Reading the log for step 2. GC logging is already on. Line 98 of the shipped file writes gc-<timestamp>.log into <install-directory>/logs with five files of 2 MB. Atlassian's GC logging article uses the same -Xlog:gc* syntax with filecount=10,filesize=5M and says "It is always useful to collect at least 10 minutes of garbage collection logs." Two Atlassian sources, two rotation sizes; the shipped one is what you have out of the box, the article's is the support-diagnostics recipe. If you want the larger rotation, set it through the hook the file provides rather than editing line 98. The export is expanded by the shell or unit file that launches Confluence, where $LOGBASEABS is not defined; the script only computes that variable for itself. So write the absolute path of your <install-directory>/logs, and put the export where the service is launched, the unit file's Environment= or the shell that runs start-confluence.sh. The path below is an example:
The article is titled for JDK 11 and 17 and does not mention Java 21; the -Xlog syntax is unchanged in the JDK 21 reference, and the 10.2.18 file ships it for its Java 21 bundle, so it holds. One shell trap: gc* is a glob, and in zsh an unquoted -Xlog:gc*:file=... fails with "no matches found" before the JVM ever sees it. Quote it in any command you paste. Here is what a correctly quoted run writes, on JDK 21:
1[2026-09-14T10:48:31.226+0300][0.005s][info][gc,init] CardTable entry size: 512
2[2026-09-14T10:48:31.226+0300][0.005s][info][gc ] Using G1
3[2026-09-14T10:48:31.228+0300][0.007s][info][gc,init] Version: 21.0.12.1 (release)
4[2026-09-14T10:48:31.228+0300][0.007s][info][gc,init] CPUs: 28 total, 28 available
5[2026-09-14T10:48:31.228+0300][0.007s][info][gc,init] Memory: 98304M
6[2026-09-14T10:48:31.228+0300][0.007s][info][gc,init] Large Page Support: Disabled
The gc,init block continues with Heap Region Size, Compressed Oops and Heap Max Capacity, which is how you confirm step 2's fifth input on the running node. The pause lines have the shape 157M->144M(512M) 2.461ms: before, after, capacity, duration; the "after" numbers across ten minutes at peak are the live set for step 2's fourth input. A Pause Full (G1 Compaction Pause) line in steady state means the heap is too small or something is leaking, and the JDK adds that "The probability to run into a Full GC can be compounded by the allocation of many humongous objects", visible as Humongous regions: X->Y lines in the same log. Do it on every node; the GC overhead page's list is "Enable logging on all nodes".
What G1 does as the heap grows, measured. Two JDK rules decide the shape of a large heap and neither has an Atlassian number: the region size "is determined ergonomically based on the heap size with a goal of approximately 2048 regions", between 1 and 32 MB, and "Humongous objects are objects larger or equal the size of half a region." I ran a synthetic allocation workload on JDK 21 at six heap sizes on a 96 GB Mac Studio, with the live set at 55% of the heap and about 200 GB of short-lived allocation per run. This is not Confluence; it is what the collector does at these sizes with the shipped flags.
-Xmx
region
compressed oops
steady young pause p50 / p95
fill-phase young pause p50, no pre-touch
same, with pre-touch
pre-touch cost at startup, by the JVM's own gc,init timestamp
8g
4M
on
0.8 / 0.9 ms
34.9 ms
1.2 ms
0.2 s
16g
8M
on
1.0 / 1.2 ms
70.1 ms
11.5 ms
0.4 s
24g
16M
on
1.0 / 1.2 ms
105.7 ms
13.1 ms
0.5 s
31g
16M
on
1.0 / 1.7 ms
137.4 ms
18.5 ms
0.7 s
32g
16M
off
1.1 / 2.3 ms
138.1 ms
13.0 ms
0.7 s
48g
32M
off
2.5 ms / tail
208.9 ms
150.0 ms
3.5 s
7 rows × 7 columnsHeader row enabled
Three things in that table travel to a Confluence node. Region size steps 4, 8, 16, 32 MB as the heap grows, so a 16 GB heap treats any 4 MB object as humongous and a 48 GB heap any 16 MB one; the JDK says nothing about Confluence, and no Atlassian page says Confluence allocates such objects, but page bodies and attachment buffers are the kind of thing to look for in the Humongous regions lines. Compressed pointers switch off at 32 GB, as step 2 said. And the gap between the two fill-phase columns is -XX:+AlwaysPreTouch, which the JDK defines as "Requests the VM to touch every page on the Java heap after requesting it from the operating system and before handing memory out to the application" and recommends alongside Xms equal to Xmx: "Avoid the delays by setting minimum and maximum heap sizes to the same value using the options -Xms and -Xmx, and pre-touching all memory using -XX:+AlwaysPreTouch to move this work to the VM startup phase ... in order to make pause-times more consistent." The delays it avoids are the tens to hundreds of milliseconds in the no-pre-touch column: first-touch page faults inside collection pauses while the heap fills after a restart, which is the first hours after a migration-window restart, with users on the node. The cost is startup time, about two-tenths of a second per 10 GB up to 32 GB here, and 3.5 seconds at 48 GB on a machine under memory pressure. The 48 GB rows carry a caveat: that machine shared its memory with other processes during the run and the tail pauses there are reported, not trusted. Atlassian says nothing about pre-touch for Confluence. It is a JDK-documented option with a JDK-documented purpose and a measured effect; whether it goes into START_CONFLUENCE_JAVA_OPTS is your call, and the honest label is that it is not on any Atlassian page.
The diagnostic pair, with a disagreement. The GC overhead page names -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/heapdump as the flags to add. A June 2026 Confluence knowledge base article on heap-space errors names the same pair and then says: "Generating a heap dump will create a file that is the same size as your JVM heap ... Also, we do not recommend this setting for heaps larger than 2gb, unless it is specifically instructed by Atlassian Support." Two Atlassian pages, one flag; on a 12 GB heap the dump is a 12 GB file written at the worst possible moment. My reading is to add the pair on the staging node where you rehearse the migration, and on production only when support asks; both positions are Atlassian's. The path is an example; create the directory first and make it writable by the confluence user, because the JVM does not create it:
How you know it worked:<install-directory>/logs fills with gc-*.log files that begin with the Using G1 line, the gc,init block shows the region size and compressed-oops state you expect, and ten minutes of them under load give you the live set and the time in GC to hold against Atlassian's Confluence thresholds: "Optimal: less than or equal to 1% of time spent in GC. Requires attention: between 1% and 5%. Needs attention: greater than 5%." On a VM, also check what the GC page tells you to check: "If you run Confluence on a VM, check that is it not using the swap file ... Instead of using swapping, ballooning and bursting, allocate adequate memory to the VM."
Step 4 — Confluence Data Center memory settings: move to the large-instance lines
Atlassian did this step for you in 7.12 and left it commented. The migration page: "we provide some recommended values for large organisations. If you're a large organisation, you can uncomment these properties, then comment out the properties for small organisations." In 10.2.18 that is lines 101 to 108. Two of the three lines are unambiguous.
The code cache. The shipped value is -XX:ReservedCodeCacheSize=256m under "Default values for small to medium size instances"; the commented alternative is 384m under "Recommended values for medium to large, and enterprise size instances". The JDK reference says the default maximum is 240 MB and the option's limit is 2 GB, so 384m is a modest, documented raise for a JIT that compiles a large application plus its apps. Comment out line 86, uncomment line 103.
The upgrade recovery file. -Dconfluence.upgrade.recovery.file.enabled=false. The recognised-properties page: "By default, Confluence creates an upgrade recovery file before and after an upgrade. The operation can take a long time on large databases and can be safely turned off if there is a process to back up the database and verify the backup before performing an upgrade." The condition is the whole sentence. If you do not have a verified database backup before every upgrade, leave this line commented. If you do, the knowledge base article adds that in a cluster "you can make this change to each node in turn, you don't need to stop all nodes at once."
The third line, -Xlog:gc*=debug, raises GC logging to debug level with the same rotation. Enable it when support asks or when you are chasing a GC problem, and do what the comment on line 107 says at the same time: comment out the ${CONFLUENCE_GC_LOG} line in the required block, because the file tells you to; on JDK 21 a second -Xlog to the same file is merged with a warning that "Output options for existing outputs are ignored". Otherwise the default gc* at info from step 3 is the steady state.
The result of steps 2 to 4 in one place, for the 32 GB node in step 2's worked example:
bash
1# Java heap size — from the step 2 calculation, not from a table2CATALINA_OPTS="-Xms12g -Xmx12g ${CATALINA_OPTS}"34# Recommended values for medium to large, and enterprise size instances5CATALINA_OPTS="-XX:ReservedCodeCacheSize=384m ${CATALINA_OPTS}"6CATALINA_OPTS="-Dconfluence.upgrade.recovery.file.enabled=false ${CATALINA_OPTS}"# only with a verified DB backup process78# Synchrony heap stays at its 2g default in this split — step 5 says when Atlassian suggests 4g
Everything else in the file stays as shipped: the JPMS arguments file, the G1 line, the GC log line, and the required block. Note what is absent. There is no -XX:MaxMetaspaceSize. The shipped file does not set one, the recognised-properties page does not list one, and the JDK default is unlimited. The OutOfMemory page mentions the pre-Java-8 MaxPermSize only to say "If your error does not reference PermGen, there is no need to increase it."
How you know it worked: System Information shows the new CATALINA_OPTS, the PrintFlagsFinal check from step 2 prints ReservedCodeCacheSize = 402653184 {command line}, which is 384 MB in bytes, and the startup line in atlassian-confluence.log carries -Dconfluence.upgrade.recovery.file.enabled=false.
Step 5 — Synchrony heap allocation: its own JVM memory
Collaborative editing runs in a separate process. "Synchrony runs in a separate JVM, and does not support direct HTTPS connections." Everything about its memory is therefore a second set of parameters, and the two mechanisms for passing them are easy to mix up.
Heap goes through a Confluence system property, in CATALINA_OPTS. The recognised-properties page: "synchrony.memory.max ... 2g ... This is the maximum heap size (Xmx) allocated to Synchrony, the service that powers collaborative editing ... The default value of this property was increased to 2 gigabytes in 7.10.0." The configuring-Synchrony page gives the cluster guidance: "If you experience out of memory errors related to Synchrony, you can change the heap size allocated to Synchrony using the synchrony.memory.max system property. If you're running Confluence in a cluster, you may want to increase the maximum heap size to 4gb on each node." Read the condition: 4g is the answer to a Synchrony OutOfMemoryError, not a starting value, and Atlassian's own 16 GB split in step 2 gives Synchrony 2 GB. The 2g default has a history: the request that produced it, CONFSERVER-58401, said the old 1 GB "is not enough for most of the deployments and it may often cause OOM problems", asked for 2 GB and suggested "a higher value (such as 4GB) should be used as the default heap size on Data Center"; 7.10.0 shipped the 2 GB, and the 4 GB survives only as that conditional sentence. One older page disagrees on the floor: the hardware requirements guide still says "the minimum heap size to allocate to Confluence is 1 GB and 1 GB for Synchrony", while the property default has been 2g since 7.10.0; read the older page as a minimum, not a default. The same half-gigabyte rule applies: the knowledge base article for raising it says "to set the memory at 3 gigabytes, configure the synchrony.memory.max to 3072m" and repeats "do not use .5 increments". Stack size is its sibling, synchrony.stack.space, default 2048k: "Increase if you experience stack overflow errors, or decrease if you experience out of memory errors from Synchrony."
Everything else goes through the args file, since 7.20: "create a file called synchrony-args.properties in your home directory (or shared home if you have a Data Center license) ... one per line", as synchrony.jvm.arg.0=..., synchrony.jvm.arg.1=.... The Data Center instruction is specific: "we recommend storing the synchrony-args.properties file in the shared home directory, so that all Synchrony processes are started with the same JVM arguments. If you do locate the synchrony-args.properties file in the local home, the arguments will only be passed to the Synchrony process on that node." And the boundary between the two mechanisms is hard: "you can't use this method for passing any value that is already handled by a Confluence system property, such as synchrony.port, Xmx or Xss."
The one thing worth putting in that file on a large site is GC logging for Synchrony. Atlassian's article gives the line for JDK 11 and 17:
Apply it without a Confluence restart: navigate to Confluence Administration » General Configuration » Collaborative Editing and "Click Restart Synchrony"; "If Confluence is running in a Cluster, you will need to restart Synchrony process on each Node (one at a time)."
What Synchrony does at scale is documented by numbers rather than advice. Its database pool is fixed: "Synchrony process used for Collaborative Editing feature has its own Database Connection Pool and will use up to 15 DB connections. This pool size cannot be modified." Concurrent editors are capped: "A maximum of 12 people can edit a page at the same time ... Administrators can increase or decrease this limit using a system property. If you experience performance issues when many people are editing, you might want to decrease this limit", and the direction of that sentence is down under load, not up. Atlassian's own instance runs Synchrony on two dedicated c5.large nodes of 2 vCPUs and 4 GiB each, a Synchrony cluster of its own, which is the standalone mode since managed Synchrony runs on the Confluence node, for 2,400 employees at 222 average and 277 peak concurrent users, and its release test harness uses one c5.2xlarge. If you run a standalone Synchrony cluster instead, Atlassian recommends "starting with 2 Synchrony nodes" and "2GB memory for Synchrony", the Confluence side needs only -Dsynchrony.service.url, and "All Confluence nodes must use the same Synchrony mode." Managed is the recommended default unless "making sure the editor is highly available is essential".
How you know it worked:<install-directory>/logs gains synchrony-gc-* files after the restart, which is the check Atlassian's article names, and System Information lists synchrony.memory.max at the value you set. On a 32 GB node with the 12 GB Confluence heap from step 2 and Synchrony at 2 GB, the operating system, the external process pool and the JVM's own off-heap memory share what is left. If free shows swap in use, the node is oversubscribed, and Atlassian's remedy is to "allocate adequate memory to the VM"; if you cannot, the heaps have to fit in what the node has, which means a smaller Confluence or Synchrony heap.
Step 6 — Pair the Tomcat thread pool with the database pool
This is a setting that can make a large Confluence slow while every JVM number looks fine, and Confluence checks it for you at startup. The shipped conf/server.xml connector in 10.2.18 is:
Forty-eight HTTP threads. The startup check that reads it: "This check confirms that the size of your HTTP thread pool (MaxThreads) is not greater than your database connection pool. For example if MaxThreads is set to 48, your database connection pool should allow 60 connections. As a general rule you should allow either 10 additional database connections or 25% more database connections (whichever is the greater), for debugging or administrative purposes."
The database side lives in the local home, in confluence.cfg.xml, and since 7.14 it is two properties set to the same value:
That is Atlassian's example for a 48-thread connector. Atlassian does not publish the default pool size on any page I could fetch, and the tar.gz has no confluence.cfg.xml because setup writes it, so read yours rather than assuming.
Which one do you raise on a high-load site? Both, together, and in a fixed order of preference. The knowledge base article for the busy-threads error says "If you're running a high-load installation of Confluence, increase the number of maxThreads in your server.xml file: From 150: To 200 or greater". That article is written for a 150-thread connector, the value in the shipped file's commented HTTPS template, and the pool-exhaustion article notes Tomcat's own default "will default to 200"; on the shipped 48-thread connector the same advice means raising maxThreads and the pool together, in the ratio above. The article on pool exhaustion says where the bottleneck should sit if there is one: "Limit the number of HTTP threads available to be closer to the number of database connections available. If you are going to have a bottleneck, then the HTTP thread pool is better than the DB connection pool since fewer resources are consumed while waiting for an HTTP connection." The method for knowing whether you need to is thread dumps: "To assess whether you need to tune your database connection pool, take thread dumps during different times (including peak usage). Inspect how many threads have concurrent database connections."
Then check the database itself can take the sum across nodes. "MySQL has a default of 151 maximum connections ... PostgreSQL has a maximum of 100 connections ... SQL Server and Oracle usually have sufficiently high limits." Four nodes at a 60-connection pool plus 15 for each Synchrony is 300 connections, three times a default PostgreSQL. Atlassian's own instance alerts at "More than 1000 connections" on a database node that "supports a maximum of 1,320 connections". And check latency, because a pool is only as fast as the round trip: the Database Latency field "should be blazing fast, and return within 1 or 2 milliseconds. If the value displayed is between 3 and 5 milliseconds, you might already have an issue. If the value is above 10ms, then you definitely need to investigate ... Don't stop improving until latency is below 2ms on average."
Two footnotes. acceptCount has no Atlassian-documented value beyond the shipped 10, and 100 in the commented HTTPS connector; leave it. And the startup check "is displayed incorrectly if you have configured Tomcat to use the Http11Nio2Protocol", so if you switched protocols, do the arithmetic yourself.
How you know it worked: Atlassian's verification on the MaxThreads check page is "Restart Confluence and confirm the startup check no longer logs the warning"; apply it to both warnings, "Startup check: HTTP MaxThreads configuration" and "Startup check: Database connection pool size", in atlassian-confluence.log. Under representative load, the verification Atlassian names is two JMX attributes: Catalina:type=ThreadPoolcurrentThreadsBusy "should peak below MaxThreads" and com.zaxxer.hikari:type=PoolActiveConnections "should peak below the pool max. If either is consistently saturated, you may need to increase BOTH together (always preserving the 25% headroom)."
Step 7 — Prepare the cluster for the Confluence Cloud Migration Assistant
Everything above keeps the instance healthy. This step is about the Confluence Cloud Migration Assistant, and it has an honest shape: Atlassian documents a floor, a limit, a rate and an order of operations, and documents no tunable at all. What the assistant actually does with your CPUs, nodes and heap is only visible in its shipped code, and this step reads that code so you know what to expect, not so you change anything.
The floor and the limit. Step 2's input one: at least 4 GB of heap, -Xms4096m -Xmx4096m, checked in setenv.sh or in the support zip's <max-heap>. Open files, from the same checklist item: "Ideally, it should be as close as possible to 32768 ... Open the application-propperties/application.xml file. Search for . If this number is close to 32768, adjust it accordingly." The article that motivates the limit is the assistant's own out-of-memory error, which is a thread limit and not a heap:
text
1ERROR [pool-49-thread-3] [service.stepexecutor.attachment.AttachmentMigrationExecutor] error Attachment uploader finished with exception -- planName: Confluence Cloud Migration | ... | taskName: Migrate space ALPHA | ...
2java.util.concurrent.CompletionException: net.jodah.failsafe.FailsafeException: java.lang.OutOfMemoryError: unable to create new native thread
Its cause, in Atlassian's words: "Migration plans including any spaces with many attachments may create thread contention on the source host ... the maximum number of open files has been reached." Its fix puts ulimit -u 4096 and ulimit -n 4096 at the top of setenv.sh, with limits.conf rows of nofile 4096 soft and 8192 hard and the same for nproc, "This value will need to match your system user that runs Confluence". So Atlassian's open-files advice for a migration is three numbers on three pages: 4096 and 8192 as the fix for the error, 32768 as the checklist's target, and the health-check page's 4096 example in between. Take the target, and take it through the service manager so it survives an upgrade that replaces setenv.sh.
Disk for the export. Atlassian gives no size, and says so in the shape of an error: "Not enough space available in the folder /var/atlassian/application-data/confluence/temp/space-export/ . You can free up space and try again or export in smaller spaces batches." The export writes under <Local-Home>/temp/, and the knowledge base article for relocating it says: "Define a node that will generate the export if running DC / Stop it / Edit the file /confluence.cfg.xml on that node / Find the following property and change the path ... ${localHome}/temp", because "You may need to change the destination of the file in case the space export exceeds the disk size where the Confluence home folder is mounted". Before the run, "Remove old log files, heap dumps, and temporary files from the Confluence home directory."
What the code does with your CPUs, nodes and heap. The recognised-properties page documents no assistant property at all; grep for the plugin's names returns zero. The assistant's shipped code, version 3.13.21 as bundled with 10.2.18, is where the mechanics are, and I report them as code, not as settings. Its concurrency comes from hardware: it treats a cluster as eligible for its parallel path when a node has at least 16 processors and the cluster has at least two nodes, it re-checks the node count every five minutes, and it reads the database's own CPU count with a query per vendor, logging "It is most likely Confluence does not have the permissions ... to read the CPU statistics, please add these and restart CCMA" if it cannot. Its per-step parallelism has shipped defaults, two for export, four for import, two for uploads, five for attachment uploads, and can be overridden from Atlassian's side; there is no -D to set it and no Atlassian page describes it. Its network client uses a 5-second connect timeout, 60-second read and write timeouts, and switches to multipart upload above 104,857,600 bytes in 16 MiB parts. Its database queries carry a ten-minute timeout when Atlassian enables that gate.
And its memory protection, which is the one mechanism that turns step 2's heap into a migration outcome. When its memory budget is enabled, the attachment executor reads heap used against heap maximum and applies three thresholds: it will not start a step above 80%, it pauses and re-checks every five seconds above 90% for up to five minutes, and it stops above 95%, logging "Memory usage exceeds tolerance for starting a step execution (Threshold: {}, Usage: {})" and "Memory usage exceeded safe maximum tolerances." A polling thread watches every 30 seconds. Nothing in the code raises concurrency when memory is plentiful; parallelism comes from cores and nodes, headroom comes from you. That is why step 2 sized the heap so that the live set sits well under 80% of it: not because a page says so, but because the assistant's own code does.
Nodes. Atlassian publishes no instruction to run the Confluence assistant on one node, to pick a node, or to disable nodes; I looked. The code is cluster-aware, with per-node and per-cluster limits and a node-count job, and the only node-specific instruction anywhere is about manual space exports. What Atlassian does say about a cluster in a migration window is in the read-only mode page and the rules in step 8: same memory configuration on every node.
Read-only mode, and what it does not do. "If you need to perform maintenance while Confluence is still running, or if you're preparing to migrate to a new site, you can put your site into read-only mode to limit what users can do. Your users will be able to view pages, but not create or change them." It is under General Configuration, Maintenance, and the migration downtime page says to use it: "Put your site into read-only mode prior to migrating to prevent your users from making changes during migration." Three caveats from its own page: "Confluence's scheduled jobs will continue to run normally in read-only mode", "read-only mode does not prevent data from being written to the database, but will significantly limit the changes that can be made", and "Not all apps ... are compatible with read-only mode, and may continue to allow users to create or update content". System administrators can still export and import spaces and manage apps and users while it is on.
The order of operations, from Atlassian's downtime page. Attachments first: "Migrating attachments is typically the longest part of the migration. We recommend that you migrate attachments before migrating other project or space data. This is because if you migrate attachments after migrating project or space, some attachments may not link correctly on the cloud because the system requires you to have the media on the cloud before your project or space data." The plan for that is "Spaces: Migrate attachments only; Users and groups: Skip users and groups; Apps: None", and Atlassian's insight says to do it "regardless of their size". Users next, and all of them: "we recommend that you pre-migrate users and groups. This is especially important for instances with over 2,000 active and inactive users", and "To migrate users and groups separately, you'll need to migrate all of them"; on the second run "the migration assistant will still show information that all users are being migrated – that's not the case, it's only the difference." Then spaces, in phases if large: "The median migration rate (based on all migrations we've observed in the past 3 months) is 400,000 pages per 24 hours. If you try to migrate more pages than that, it's likely that your migration will take longer than 24 hours ... your goal is to reduce the number of pages you migrate in a single migration plan", and "If you've many spaces and attachments or you are on Data Center, you might want to break the migration up into a few smaller migrations. The migration assistant can be slow to load and process tasks when there is a lot to manage." Two running at once do not overlap: a new plan involving users and groups "will wait 60 minutes for the previous migration to complete."
Clean up before, never during, and rehearse on a clone with its mail off. "Don't attempt to clean up your instance while a migration is running. The best time to clean up your data in Data Center is before you run a migration." The staging clone needs its production Server ID: "save the staging Server ID (Administration > System > System info) before copying the production data to your staging environment. After you clone the production, the last step is to update the staging Server ID to the saved value." And Atlassian's staging page cuts its mail at the database rather than with a flag; the recognised-properties page does list -Datlassian.mail.senddisabled=true and -Datlassian.mail.fetchdisabled=true, both default false, but the clone page's instruction is the query: "Disable global outbound mail by running the following database query: SELECT * FROM BANDANA WHERE BANDANAKEY = 'atlassian.confluence.smtp.mail.accounts'; Disable space-level mail archiving ...: SELECT * FROM BANDANA WHERE BANDANAKEY = 'atlassian.confluence.space.mailaccounts'; Change the 'SELECT *' to a 'DELETE' in the above queries once you are sure". Test into a Cloud site that is not your production organisation, because "there's no way of cleaning up duplicate entries after the migration."
Network and timezone. "Right before your production migration, we recommend you perform a network test on the server ... Ensure that your production environment doesn't have any additional security controls (e.g. egress traffic scanners) that may slow down data upload into the Atlassian cloud environment." And one you would not guess: "We recommend that you don't change the server timezone if you plan to migrate from server to cloud."
The Cloud limits that bound what arrives. These are not JVM settings, and a large migration fails on them more often than on memory. Users: "If you're migrating to a Confluence Cloud site that's on the Free plan ... can't exceed 10 or the migration will fail. If your Confluence Cloud site is on an annual subscription to another plan ... doesn't exceed your subscription user tier." Atlassian's plans page gives Standard's ceiling as "up to 20,000" in one sentence and "10+ to 10,000" in another, on the same page; check your own plan. Storage: "Confluence Free- 2 GB Confluence Standard- 250 GB Confluence Premium- Unlimited storage", and "Archiving content that contain attachments in Confluence will not free up any storage." Attachments: Cloud's per-file default is "100 megabytes, expressed as 104857600 bytes", the same number at which the assistant switches to multipart. Space permissions: the one printed guardrail is "20,000 permissions per space". And what does not migrate at all, from Atlassian's list: space shortcuts, Team Calendars, watchers, "Save for later", user avatars, passwords, personal drafts, custom emojis, and attachments added as a link address or stored through the mail archiving plugin.
One more thing worth knowing exists. Atlassian's Hosted Cloud Migration Assistant, available in August 2026 "only to large customers who qualify a set of criteria", moves the heavy processing off your nodes: "Heavy migration processing runs on Atlassian-hosted infrastructure rather than your finite on-prem hardware." If you qualify, most of this step becomes theirs.
How you know it worked:cat /proc/<confluence-pid>/limits shows the raised open-files and process limits; the test migration on the clone finishes without the native-thread error or the temp-directory error; the assistant's log shows no "Memory usage exceeds tolerance" lines, a check that only fires when Atlassian's memory-budget gate is on, so read it with the GC log and not instead of it; and the test's duration, divided into its page count, is near Atlassian's 400,000-a-day median or you know why it is not.
Step 8 — Confluence Data Center load profiles, the cluster rules, and what Atlassian tested at scale
Two settings outside the JVM decide whether the JVM you sized stays in the cluster, and then the question a 20,000 to 30,000-user site actually asks: how many nodes, how big.
Open files are in step 7. The heartbeat is where step 2's "keep the heap small" turns from advice into a cluster rule. Hazelcast evicts a node that misses heartbeats, the default is confluence.cluster.hazelcast.max.no.heartbeat.seconds at 30, and the clustering page's table spells out what a garbage-collection pause does at each setting. With the cluster safety job at one minute and the heartbeat at one minute: "You could have network interruptions or garbage collection pauses of up to 1 minute without triggering a cluster panic. However, if two nodes are no longer communicating, conflicting data could be being written to the database for up to 1 minute." With both at 15 seconds: "Network interruptions or garbage collection pauses longer than 15 seconds will trigger a cluster panic." The page's own verdict: "In most cases the default values will be appropriate." Atlassian's own instance disagrees with its own default in one direction: "By default, Hazelcast removes and re-adds a node if it doesn't send a heartbeat within 30 seconds. We configured ours to do this within 60 seconds instead", and it alerts on "Any Garbage Collector pause that lasts longer than 5 seconds", with the note that "Usually this alert requires no action ... If the Garbage Collector triggers this alert frequently, we check if the instance requires heap tuning." Read your GC log from step 3 for the longest pause; if a collection on your heap approaches the heartbeat, the heap is too large for its collector, which is what the OutOfMemory page meant by "Increasing the heap beyond the capabilities of your server to adequately Garbage Collect can cause performance problems or freezing."
The consistency rule, from the clustering page's list of what all nodes must share: "have the same OS, Java and application server version; have the same memory configuration (both the JVM and the physical memory) (recommended)". setenv.sh is per node, in the install directory; there is no shared copy, so the same file goes on every node by your hand. On node count: "Your Data Center license does not restrict the number of nodes in your cluster. The right number of nodes depends on the size and shape of your Confluence site, and the size of your nodes ... In general, we recommend starting small and growing as you need."
What Atlassian has tested. Atlassian's tiers first, from the load profiles page. Content in all versions: Large is 2.5 to 10 million, XLarge 10 to 25 million. Local users: Large is 10,000 to 100,000, XLarge 100,000 to 250,000. Traffic: Large is 350,000 to 700,000 HTTP calls an hour, XLarge 700,000 to a million. So 20,000 to 30,000 users is a Large site by Atlassian's user metric, and the page has no concurrent-user axis at all; the only Confluence concurrency figures Atlassian publishes are its own instance's 222 average and 277 peak, and the 565 and 1,200 "active users" of its Large and XLarge test datasets. Nothing Atlassian has published load-tests 2,000 concurrent Confluence users.
The extra-large benchmark is the one to read for a site your size. Dataset: 34.9 million content items, 10,500 spaces, 102,000 local users, 1,200 active, a million HTTP calls an hour, on Confluence 6.13 and 6.15. Result: "Among the virtual machine types we tested for the application node, the only one that performed at an Apdex above 0.8 was c5.4xlarge. This virtual machine type did so at 3 or 4 nodes. Once we get down to 2 nodes, Apdex will dip below 0.8." Stability configuration: four c5.4xlarge nodes of 16 CPUs and 32 GB, one m4.2xlarge database, "The Stability configuration can maintain acceptable performance (that is, Apdex above 0.8) even if it lost one application node." Heap: 8 GB. The Large benchmark, 16 million content items and 12,300 users at 498,000 calls an hour, found that "when it comes to Confluence's performance, vertical scaling works better than horizontal scaling. Or, you'll get better performance on less nodes with more powerful hardware than more nodes with less powerful hardware", that "Beyond four nodes, there aren't any considerable changes in performance", and that a bigger database node "showed a slight regression in performance." Both pages carry Atlassian's caveats: no apps installed, older versions, "adding more application nodes to a growing Medium-sized instance doesn't always improve performance (in fact, the opposite might happen)", and the Apdex threshold of 0.8 that Atlassian relaxes to 0.7 for its own production, which "takes into account the performance impact of apps". Next to that, the Jira extra-large test on the same family of pages used 36-CPU, 72 GB nodes; Atlassian sizes a Confluence extra-large node at under half a Jira one.
Atlassian's own production Confluence, as of its 2018 sampling: 2,400 employees, 14.9 million content items, 6,500 spaces, 11,000 local users, 341,000 HTTP calls an hour averaging 456,000 at peak, 70 apps, on four c5.2xlarge nodes of 8 vCPUs and 16 GB, two Synchrony nodes, one m4.xlarge database, and the 60-second heartbeat above. It is "Large for both content and traffic" by Atlassian's own profiles, and it states no heap. The release test harness Atlassian runs on every release candidate uses four c5.2xlarge nodes on a 16-million-item dataset with 360 logged-in users generating 431,000 HTTP requests an hour, "one of the 10 heaviest we've ever seen in production", and also states no heap.
Two more Atlassian numbers for the migration window. Rate limiting for REST is per node, not per cluster: "each of your users will have a separate limit on each node (rate limits are applied per node, not per cluster)", and it never touches UI traffic: "Rate limiting targets only external REST API requests". And the fossil still live in the 10.2 documentation tree: the hardware requirements guide's "Maximum reported usages" table says "Most Internal Users 15K / Most Pages 80K", numbers dwarfed by Atlassian's own instance and its own extra-large test; do not size from that page.
What drives the memory is not the user count. Atlassian's memory page names caches, large-attachment indexing, backup and restore, and the mail error queue; the cache tuning page gives the rules of thumb, the content objects cache "should be set to at least 20-30% of the number of content entity objects", counted with select count(*) from CONTENT where prevver is null, the space permissions cache "20 times the number of spaces", each grown only when a cache shows "a high usage percentage (above 75%)" and low effectiveness, with the caveat that "In cases of large databases, 20-30% of the size of the table may be unnecessarily large." Cache sizes take effect without a restart and live in <shared-home>/config/cache-settings-overrides.properties in a cluster, but "To flush a cache or adjust the size, you'll need to access the Cache Management screen on each node directly."
Practitioner reports, labelled as such. These are not Atlassian guidance and not verified instances. A Platinum partner's system-requirements page for its own intranet suite on Confluence says "For a 2,000-user confluence system, this means approx. => 8-10 GB heap space. The Java code cache should be at least 512 or 768 MByte", for licensed users rather than concurrent ones and for a Confluence carrying that suite, with the partner's own caveat that "these are only average orientation values" and that "an optimized heap results from the monitoring data and its analysis". A Community Champion's 2021 article shows what a 32 GB heap looks like when it goes wrong, on Jira Service Desk and JDK 8, not Confluence: "[Full GC (Allocation Failure) 31309M->23165M(32768M), 58.7721842 secs]", five of them in five minutes, improved but not cured by JDK 11. Atlassian's marketing names one customer running Confluence "at scale for 10,000+ users" with no numbers behind it. I found no first-hand report of a 20,000 to 30,000-user Confluence Data Center with its heap, nodes and Synchrony stated, in eighteen searches; Atlassian's extra-large test is the only public data point at that user scale, and it is Confluence 6.x era.
How you know it worked: Administration, System, Clustering shows every node with the same memory column and no evictions in atlassian-confluence.log; the open-files health check reads under 70%; and the longest GC pause in ten minutes of logs is a small fraction of 30 seconds, and under Atlassian's own 5-second alert.
Step 9 — Verify it took, on every node, and measure the test migration
Do this after every change and on every node, because the failure mode of setenv.sh is silent. Confluence starts either way.
The startup log first. <local-home>/logs/atlassian-confluence.log prints the JVM arguments at startup; search the last startup for -Xms and -Xmx, as the OutOfMemory page says, and for ReservedCodeCacheSize and synchrony.memory.max. Then System Information under General Configuration, scroll to System Properties, which shows what the running JVM actually has. Then the last startup in atlassian-confluence.log for the two startup-check warnings from step 6, and Troubleshooting and support tools, Instance health, for the open-files check from step 7. Then <install-directory>/logs for gc-*.log and synchrony-gc-*.log growing.
Then the two numbers Atlassian defines. Time in GC from the logs, against 1% and 5%. Database latency from System Information, against 2 milliseconds. If both are green and users still report slowness, Atlassian's performance-support page says what to collect before anyone touches a flag: "A support zip, containing log files and configuration, ideally with a series of thread dumps separated by 10 seconds", and the question it asks first, "How much memory is assigned to Confluence's JVM? (i.e. what are the -Xmx and -Xms settings for the JVM?)", is the one this tutorial has already answered.
Then the migration, on the clone. Atlassian: "We recommend running a test migration to work out how long your migration may take, document the specific processes you'll need to follow in a runbook". Its duration against its page count, against the 400,000-a-day median. Its log for the three strings this tutorial has named: unable to create new native thread, Not enough space available in the folder, and Memory usage exceeds tolerance. Each one maps to a step above, and none of them is a reason to raise the heap without reading the GC log first.
Two things that are not JVM parameters but move the needle more than any of them, both from the performance tuning page. Caches, as in step 8. And antivirus: "Antivirus software greatly decreases the performance of Confluence"; exclude the home directory, the index directory and the database directories.
There is no recommended Confluence heap size in it, because Atlassian does not publish one; there is a calculation from the floor Atlassian requires, the splits and tested heaps it publishes, and your own GC log. There is no Metaspace cap, no pause-time target, no thread count, no region size, because no Atlassian page sets them for Confluence and the shipped file does not either. The three flags you saw for G1 are the three Atlassian ships, left exactly as they are. The migration assistant's concurrency, timeouts and memory thresholds are described as what its shipped code does, because no Atlassian page documents them and none of them is a setting you should change. Pre-touch is in, measured and labelled JDK-only, because the JDK documents both the flag and the reason and the effect was large enough to show. Where two Atlassian pages disagreed, on the heap increment, the GC log rotation, the open-files number, the heap-dump flag, Synchrony's minimum against its default, the Standard plan's user ceiling, and the checklist's "Recommended" against "[mandatory]", both values are here. The local reproductions are on OpenJDK 21.0.12.1 on a Mac, which is the same major version Confluence bundles and not a Confluence node, and the sweep workload is synthetic.
That is what "known good" means before a migration: not a configuration that worked once, but one where every line can be traced to the vendor that has to support it, every number is either Atlassian's or measured, and the one calculation that is yours is done from your own GC log.
Key takeaways
Start from the shipped setenv.sh. The large-instance block is already written at lines 101 to 108; Atlassian says not to copy an old file forward.
The heap is a calculation. 4 GB floor for the assistant; Atlassian's splits, 3 of 10 and 8 of 16; its tests, 8 GB at 102,000 users and 12 GB on its toolkit; your GC log's live set divided by about 0.4; never 32 to 47 GB; Xms equals Xmx; no .5 values.
G1 as shipped; pre-touch is JDK-only, measured at 6 to 11 times shorter fill-phase pauses between 16 and 32 GB for about two-tenths of a second of startup per 10 GB; the heap-dump pair is on one Atlassian page and discouraged above 2 GB on another.
Uncomment 384m code cache; disable the upgrade recovery file only with a verified backup process.
Synchrony: 2g default, 4g only on a Synchrony OutOfMemoryError in a cluster; other flags in the shared-home args file; 15 connections fixed; 12 editors, lowered under load.
Database pool at least maxThreads plus 10 or times 1.25; nodes times pool plus 15 per Synchrony against the database limit; latency under 2 ms.
For the migration: 4 GB, open files near 32,768, disk under the local home's temp, read-only mode on, attachments first, users first above 2,000, phases at 400,000 pages a day, a clone with its mail deleted. With its memory budget enabled, the assistant's code stops starting steps above 80% heap use; its parallelism comes from 16 cores and 2 nodes, not a property.
Same heap and RAM on every node; heartbeat 30 seconds by default, 60 on Atlassian's own instance. Atlassian's largest Confluence test: 102,000 users, 8 GB, four 16-core nodes; fewer bigger nodes won.
Verify on every node: startup log, System Information, the startup-check warnings, Instance health, GC under 1%, latency under 2 ms, and the test migration's log for the three named strings.