Scale and size a contribution campaign#
A contribution form is not free. Every form a contributor opens becomes a live workbook in a worker process on the server, and that memory is held for as long as the form stays open. A server admits new contributor sessions only while it has memory to spare, so the cost of one form and the size of the server together decide how many people can contribute at the same time.
This guide is about the decisions. For the click-by-click mechanics of each measurement tool, see Profile and stress-test a campaign; this guide explains what to measure, how to read it, what arithmetic turns it into a server size, and how to confirm the answer while a real round is running.
The worked example is a Q3 budget round: one budget form per cost center, several hundred contributors, all of whom submit in the last week.
Before you start#
- The campaign has generated forms. Every measurement reads the generated form workbooks.
- You can open Campaign profiling from the Campaign Dashboard.
- For the live checks in Watch a live round, you need administrator access to Settings ▸ Performance.
- For a stress test, prepare a disposable copy of the campaign, a dedicated stress-test user, and one or more Windows load generators, as described in Profile and stress-test a campaign.
The sizing loop#
Run these six steps in order. Steps 1 to 3 repeat until the form is as cheap as you can reasonably make it; steps 4 to 6 happen once per deployment decision.
- Design the form. Author it as you want contributors to see it.
- Measure its footprint. Profile one form on the Memory profiling tab, and record the numbers.
- Tune and re-measure. Change one thing, profile again, and compare. Only a before-and-after pair on the same machine is meaningful.
- Size the deployment. Combine the measured cost per form with the server's usable memory to get a concurrency number, then a server count.
- Prove it under load. Build a stress plan at the concurrency you sized for, run it, and read what contributors experienced.
- Watch the live round. During the real campaign, confirm on the contribution process console (Settings ▸ Performance ▸ Contribution) that the sizing held.
What limits a server#
Two separate ceilings decide when a contributor is turned away. They fail differently, so diagnose them separately.
The memory admission valve. When a contributor opens a form, Reportworq checks every online server (the local one and any load-balanced servers) and steers the new session to the online server with the most available memory that is still able to accept one. A server can accept a session only while its memory used is below 90%. If no server qualifies, the form does not open and the contributor sees:
Unable to launch new Contribution session because all servers are at capacity. Please try again later
This is a hard refusal, not a warning or a queue. It is the reason per-form cost matters: usable capacity is bounded by that 90% wall, not by the machine's total RAM.
The per-worker session cap. Contribution sessions run in worker processes, and Max Sessions per Worker (on Settings ▸ Configuration ▸ Performance, under Contribution) limits how many sessions one worker process hosts. Its default is 1, so by default every live form is its own process. If workers report At capacity while server memory is still comfortable, the session cap is binding, not the form's footprint. See Server configuration.
What drives a form's cost#
Reportworq measures five things about a form's shape. The Memory profiling tab reports sheets, cells, and input cells alongside the workbook size; the Form sizing table on the Stress testing tab adds the formula count.
| Measured property | Where it appears | Why it matters |
|---|---|---|
| Cells | Memory profiling, Form sizing | The count the per-cell cost is multiplied by. This is the dominant term. |
| Sheets | Memory profiling, Form sizing | Cells are counted across every sheet, so an extra sheet adds its whole cell block. |
| Input cells | Memory profiling, Form sizing | The cells a contributor can edit, and the ones a stress test fills. |
| Formulas | Form sizing | Formula cells across the workbook. Recalculation is part of what the profiler exercises. |
| File size | Memory profiling, Form sizing | The generated workbook on disk, a quick relative check between forms. |
The profiler reports Bytes per cell (at peak), which is simply the measured peak cost divided by the cell count. Read the relationship in that direction and it stays honest: peak cost is roughly bytes per cell multiplied by cell count, so the reliable way to make a form cheaper is to make it smaller. Removing an unused sheet, or trimming a form down to the range contributors actually fill, removes cells and therefore removes cost, and the profiler will show you exactly how much.
Two things the measurements do not tell you, so treat claims about them as judgment rather than fact:
- The profiler measures the whole form's footprint. It does not attribute cost separately to formulas, formatting, charts, or images, so it cannot confirm that removing any one of those saves a given amount. If you think one of them is expensive, remove it, profile again, and let the before-and-after numbers decide.
- Bytes per cell varies between forms. A form whose bytes-per-cell figure is far above your other forms is worth investigating, but the number does not say what is responsible.
One campaign setting is a genuine, verifiable memory lever. Manual calculation mode (in the campaign's Options section) stops the form recalculating on every edit; contributors press Recalculate instead. Under manual calculation the server releases the session's workbook 30 seconds after the last action that needed it, and rebuilds it on demand when the next one arrives, so an idle contributor who has left a form open costs far less than one in automatic mode. The trade is that a contributor waits for the reload on their next action. For large forms filled slowly over a day, that trade is usually worth making. See Author a contribution campaign.
Measure the footprint#
Use the Memory profiling tab in this order. The click path for each run is in Profile and stress-test a campaign.
- Profile your heaviest single form first. Select one form, leave Flip through sheets and Enter sample data on, and run it. This gives the detailed per-form footprint.
- Tune, then re-run the same profile. Change one thing at a time and compare against the first run. Do the comparison on the same machine; the absolute numbers move with hardware, the deltas do not.
- Profile several forms together. Select the forms a real round will have open at once and run again. This is the run that produces the marginal cost per form, and it is the number the sizing arithmetic uses.
Read the results like this:
| Number | What it tells you | How to use it |
|---|---|---|
| Peak per form (avg) | The working memory one open form costs at its busiest, averaged over the scored cycles. | The headline cost of a form. Compare before and after every change. |
| Steady (just opened) | The cost right after the form opens, before any interaction. | The floor a form costs simply by being open, which is what most contributors hold most of the time. |
| Bytes per cell (at peak) | Peak cost divided by the cell count. | A size-independent comparison between forms. Use it to spot a form that is expensive for its size. |
| Held after close | Memory still held after the form closed, measured against the idle floor. | A retention signal. A small figure is normal; a figure that climbs cycle after cycle is not. |
| Worker baseline | What a worker process costs before any form is open. | Add it per session when workers host one session each, which is the default. |
| Marginal cost per form | The true cost of each additional concurrent form, measured after the first. | The concurrency number. Use this, not the first-form cost, for the arithmetic. |
| First form | The first concurrent form's cost, which carries one-time warm-up. | Do not multiply this. It is a one-off. |
The single-form result also shows an estimate reading ~N of this form open at once on the profiling machine. It is derived from the free memory on the profiling machine, taking 80% of it and dividing by the average peak per form. Treat it as indicative only: it reflects the machine that ran the profile rather than production, it ignores the worker baseline, and it ignores the per-worker session cap. Use it as a sanity check on the arithmetic below, not as the answer.
Turn the measurements into a sizing answer#
Work through the arithmetic with your own measured numbers. The figures below are illustrative.
What was measured for the Q3 budget round:
- Peak per form (avg): 180 MB
- Worker baseline: 100 MB
- Marginal cost per form: 120 MB
What the server offers:
- Total memory: 64 GB
- Memory in use when no forms are open (operating system, Reportworq, everything else): 12 GB
Step 1: decide the memory you will plan to. The valve refuses at 90% used, so never plan to consume the full 90%. Plan to 80% and keep the last ten points as headroom for spikes and for other work on the machine.
80% of 64 GB = 51.2 GB
minus the 12 GB idle floor = 39.2 GB (40,140 MB available for sessions)
Step 2: work out the cost of one concurrent contributor. This depends on Max Sessions per Worker.
- At the default of 1, each session is its own worker process, so it pays the baseline itself:
100 MB baseline + 180 MB peak per form = 280 MB per contributor. - With sessions multiplexed at, for example, 10 per worker, ten sessions share one baseline:
(100 MB + 10 x 120 MB) / 10 = 130 MB per contributor.
Step 3: divide.
Default (one session per worker): 40,140 / 280 = 143 concurrent forms
Multiplexed (10 per worker): 40,140 / 130 = 308 concurrent forms
Step 4: convert concurrency into contributors. Nothing measures how many of your contributors are online simultaneously; you have to estimate it from how the round behaves. If roughly one in four of the roster is filling a form at the busiest moment, a server carrying 143 concurrent forms supports a roster of about 570 people.
Step 5: convert contributors into servers. For a roster of 1,200 at the same one-in-four peak, the round needs about 300 concurrent forms. At 143 per server that is 2.1 servers, so provision three. Always round up: the consequence of being one server short is contributors being refused, not contributors waiting.
The assumptions this arithmetic rests on. State them alongside the answer, because each one can move it:
- The measurements come from the machine that ran the profile. Re-measure on hardware that matches production before committing to a server count.
- The idle floor is whatever the server is already using. Read it from the live per-server percentage on the contribution process console rather than assuming it.
- The peak-concurrency ratio is your estimate, not a measurement.
- Contribution is not the only load. Report and job execution take memory from the same machine at the same time.
- Planning to 80% rather than 90% is deliberate. The valve is a refusal, so the margin is what stops a normal spike becoming an outage.
Prove the experience with a stress test#
The arithmetic says how many forms fit. A stress test says whether contributors have an acceptable experience at that number. Build the plan on the Stress testing tab so the run answers the sizing question:
- Concurrent forms / node multiplied by Nodes (machines) should add up to the concurrency you sized for. The Intended peak concurrent stat on the plan preview shows what the plan will actually attempt.
- Nodes matter because the forms are partitioned across them; each form runs on exactly one node. If you need more concurrency than one load generator can drive, add nodes rather than raising the per-node number indefinitely.
- Editor pace decides whether the run is a burst or a soak. Slide toward Slow & deliberate for a believable round where contributors hold forms open for a long time, which is the shape that actually tests memory. Slide toward Fast only when you are testing throughput rather than capacity.
- Leave the Advanced settings alone unless you have a reason. The defaults fill 50% to 75% of each form's input cells and submit 25% of the time, which is closer to a real round than anything you are likely to invent.
Then read the Analysis tab against these questions:
- Did the run reach the concurrency you sized for? Check Peak concurrent forms in the Run summary. If it is well below the intended peak, the run did not test what you meant to test.
- Were there errors? The Actions stat carries the error count. Errors at load are a capacity answer, not a detail.
- What did contributors feel? The experience table gives each action in seconds as Typical (half of users), Most users (9 in 10), and Slowest seen (the worst single measurement). Opening a form is the number contributors judge the system by; entering a value, recalculating, and changing worksheets are the ones they feel repeatedly.
- What did Reportworq flag? A slow tail finding means the slowest 1 in 100 measurements took at least three times the typical time, so most contributors were fine and a few were not. A slowdown finding means an action in the last third of the run averaged at least 1.5 times the first third, so something accumulated as the run went on.
Read the outcome this way:
- This scales when the run reached the intended peak, errors are negligible, in-form actions stay around a second or less, form opens stay acceptable for your users, and there are no findings.
- This does not scale when the run produced errors, when a slowdown finding appears (the deployment degrades the longer the round runs, so a real multi-day round will be worse), or when the slow tail is severe enough that a meaningful share of contributors would be waiting.
If a run fails, halve the concurrency and run again. The concurrency at which the findings disappear is your real per-server number, and it replaces the arithmetic.
Watch a live round#
Settings ▸ Performance ▸ Contribution is the operations console for the contribution runtime. It shows the present moment only, refreshing every 10 seconds, and it keeps no history. There are no timings, no percentiles, and no record of yesterday. It also identifies a session by input-form name only, so with two campaigns live at once you cannot attribute load to one or the other from this screen. Use it to confirm that the sizing held and to act when it did not, and use Campaign profiling for anything you need to measure or keep.
Full field-by-field detail is in Contribution processes. During a live round, three readings matter:
- The per-server memory percentage in the left rail, next to each online server. This is the number the admission valve reads. Watching it approach 90% is the warning that contributors are about to be refused.
- Sessions-alive in the selected worker's memory breakdown. It is the worker's working set minus its idle baseline, so it is the cost of the live sessions on that worker. Divide it by the worker's active session count and you have an empirical megabytes-per-contributor figure to check your sizing against. Two caveats: the baseline reads 0 until the worker has reported it, and the figure covers every session on the worker, so it is only a clean per-form number when one form is in play.
- The worker state. At capacity means the worker has reached Max Sessions per Worker, which is a session-count limit and has nothing to do with memory. The other states are Active, Draining, Warm, Idle, Legacy 1:1, and Stopped.
Two readings on this screen are easy to misread:
- Headroom is not free machine memory. It is the distance between this worker and the busiest worker on the same server. A large headroom figure does not mean the server has room.
- The memory bars are scaled per server, relative to that server's busiest worker. Bars on different servers are not comparable to each other.
When the valve starts refusing sessions, free memory now and re-size afterwards:
- Select the server whose memory percentage is highest, then its hottest worker.
- Kill session on a session that is clearly abandoned. The contributor's unsaved edits are recorded and the form lock is released, so this reclaims a stuck session without losing work.
- Force GC on the worker to reclaim what it can.
- Drain a worker so it stops taking new sessions and exits when its current ones end, or Shutdown it if you accept ending its live sessions.
- After the round, treat the refusal as a sizing failure, not an incident. Re-measure the form, redo the arithmetic, and add memory or a server.
Red flags and what to do#
| Symptom | Likely cause | What to do |
|---|---|---|
| Contributors are told all servers are at capacity | The deployment is undersized for the peak, or the round peaked higher than estimated | Free memory on the hottest server now, then redo the sizing arithmetic with the real peak and add memory or a server |
| Workers read At capacity while server memory is comfortable | The per-worker session cap is binding, not memory | Raise Max Sessions per Worker on Settings ▸ Configuration ▸ Performance, then re-measure the per-session cost, which falls when sessions share a worker baseline |
| Held after close climbs cycle after cycle | The form is retaining memory after it closes | Re-run the profile to confirm, download the CSV, and raise it with support. Until it is resolved, size on the retained figure as well as the peak |
| Bytes per cell far above your other forms | The form is expensive for its size, for a reason the profiler does not attribute | Remove one suspected element at a time and re-profile. Let the deltas identify the cause |
| A slow tail finding on opening a form | Contention at peak concurrency, so most contributors are fine and the unlucky ones wait | Re-run at half the concurrency. The level at which the finding disappears is the per-server number to size to |
| A slowdown finding on any action | Something accumulates over the run, so a long round degrades | Do not open the round at this size. Re-run at lower concurrency and confirm the finding is gone |
| High Served (lifetime) against a short Uptime | Workers are recycling often, so contributors repeatedly pay a cold start | Review Recycle Worker After Sessions on Settings ▸ Configuration ▸ Performance; its default is 25 |
| A server shows a last seen note instead of workers | The server has not reported for more than 5 minutes and is treated as offline | Check the server. While it is offline it takes no sessions, so the remaining servers absorb the whole round |
Notes and limits#
- Memory profiling is read-only and never changes campaign state. Stress testing writes data and submits forms, so it must run against a disposable campaign that is reset before every run.
- Profiler numbers are working set on the machine that ran the profile. Deltas between runs are dependable; absolute numbers are only as representative as that machine.
- The contribution process console is live only. Capture anything you want to keep at the time you see it.
- The concurrency estimate on the Memory profiling tab ignores the worker baseline and the per-worker session cap, so the arithmetic in this guide will usually give a lower and more realistic figure.
Related topics#
- Profile and stress-test a campaign
- Author a contribution campaign
- Contribution (write-back) overview
- Contribution processes
- Deployment topology
- Performance and capacity planning, for how contribution sizing sits beside report and job execution on the same servers
Feedback on this page
Comments, questions, requests, or something missing or unclear? Email us - the page you are on is filled in for you.
Email feedback on this pageOr write to support@reportworq.com directly.