A dashboard that takes a minute to open does not get used. This guide explains where the time goes when mWater loads a visualization, and how to design surveys, tables, expressions, and dashboards so that time stays low. The advice is ordered from the foundation up: fix the data source first, then the expressions, then the widgets, then the dashboard around them. A slow dashboard fixed at the top layer usually stays slow. One fixed at the bottom layer often needs no other work.
Every chart, table, map layer, and live text widget on a dashboard is a database query. When a dashboard opens, mWater runs those queries against the live data, with your permissions applied, and returns only the results. Three things decide how long that takes.
How many rows the query has to read. A survey with 200,000 responses costs more than one with 2,000, unless the query can jump straight to the rows it needs through an index. Filters, and the way they are written, decide whether it can.
How many other tables the query has to visit for each row. An expression like "Community name of the water point of this response" makes the database look up the water point, then the community, for every response it scans. On a small survey this is invisible. On a large one it is the single most common cause of timeouts.
How many queries run at once. A dashboard runs up to 10 widget queries at a time, starting with the widgets nearest the top. A page with 40 widgets queues the rest. A dashboard with sections runs only the widgets in the open section.
Dashboards also cache their results. By default, a dashboard opened from its saved link shows results up to 8 hours old, and the Refresh button forces a new query. The "Structure the dashboard" section explains how to tune this. Caching hides slowness from the second viewer, but not from the first one, and not from anyone who presses Refresh.
Most performance problems are decided before a dashboard exists. Set up the data source correctly and the visualizations on top of it stay fast.
Survey responses are stored in a flexible format that handles any survey design, but it is slow to filter and aggregate at scale. mWater can build an optimized copy of a survey's responses with one database column per question, indexed for querying. Dashboards, maps, datagrids, and calculated data sources then use the optimized copy automatically.
Survey administrators can start this themselves. Open the survey, go to the Settings tab, and find the Optimize Survey button at the bottom of the Options card. It is available for deployed surveys only. Optimization runs in the background and can take a while for large surveys. The button shows progress and changes to "This survey has been optimized for fast querying" when finished. New responses are added to the optimized copy as they arrive, so this is a one-time step.
mWater also optimizes surveys automatically once a day when they pass 10,000 responses, or when they are found to be causing heavy database load even at smaller sizes. Pressing the button simply gets there sooner. Optimize any survey that feeds a dashboard, and do it before building the dashboard rather than after users complain.
Three details matter for advanced users:
Only the first five questions in a survey receive their own database indexes, along with every site link, choice, and geometry question. Put the questions people will filter on most (site, region, date, status) near the top of the survey design. This helps the app user too, since enumerators reach the key questions first.
Calculation fields that reference other tables (for example, a calculation that reads a value from the linked site) are stored at the time the response is optimized. They do not update when the referenced site changes. If a value must always reflect the current state of another table, compute it in the dashboard expression or in a calculated data source instead of in the survey calculation.
One-to-many relationships (rosters, and "all responses of this site") are not part of the optimized copy and remain slower to aggregate. Keep rosters for data that is truly repeating, and avoid placing the questions you aggregate most inside a roster when they could be top-level questions.
A site link is the fastest join in mWater. When a survey has a Site question, each response stores the site's identifier, and expressions that go from the response to the site (or the reverse) follow a direct, indexed path. Surveys that instead record a site name, code, or coordinates as free text force every expression to match text, which the database cannot index in the same way and which breaks silently when a name is spelled differently.
Use one Site question per survey. If a survey needs to refer to a second site (for example, a household survey that also records the water point used), add a second Site question rather than a text question. Several Site questions in a survey are fine; text that has to be matched later is not.
Custom tables give full control over structure, so use it. Every table should have a stable identifier and, where it relates to another table, a reference column of type ID that points at that table's identifier. Set a label column so users see names while the database joins on IDs.
Avoid storing the same fact in two places. If a district name lives in a regions table, other tables should store the region's ID, not repeat the name. Repeated text drifts out of sync and forces name matching in every expression that touches it.
Keep tables narrow. Very wide tables with hundreds of columns slow every export and every table widget that selects them. Split rarely used detail into a related table.
A calculated data source runs a query on a schedule and stores the results as a table. Anything that is slow, complex, or does not need to be live belongs here: population within a buffer of each site, percentage functional by district, counts across several site types, or a join across several surveys that a dashboard would otherwise repeat for every viewer.
Use a calculated data source when the dashboard must open fast, when daily or weekly data is acceptable, when several data sources need to be combined into one, or when a live expression times out. The Calculated data sources guide walks through four worked examples. Add a column with the formula Now so viewers can see when the data was last calculated, and set the schedule no more often than the underlying data changes.
A calculated data source is also the right place for "latest value" logic. Expressions such as "the most recent functionality status of this water point" must sort every linked record for every row, and they cannot be sped up by the query engine. Compute the latest value once in a calculated data source and let the dashboard read it as a plain column.
When several surveys collect the same measure in different ways, indicators let each survey map its own questions to a common set of fields. Dashboards then query the indicator instead of stitching surveys together with conditional expressions. Indicator values are written once when a response is finalized, so reading them is cheap. Create indicators with us info@mwater.co.
The expression builder makes many calculations possible. Not all of them cost the same. The difference between a fast expression and a slow one is usually whether the database can use an index, and that depends on choices the builder leaves to you.
When an expression filters or groups by a linked record, choose the record itself, not its name. In the builder, filter with "Water point is [selected water point]" rather than "Water point > Name is 'Kibera Borehole 3'". The first form compares stored identifiers directly and needs no lookup at all. The second form makes the database open the water point for every response, read its name, and compare text.
The same applies one level up. "Region is Nakuru County" (selected from the region list) compiles to an indexed comparison on the response's stored region. "Region > Name is 'Nakuru'" does not, and it fails when the region is renamed or the name has a trailing space.
IDs also survive data cleaning. A site can be renamed, merged, or corrected without changing its identifier, so dashboards built on IDs keep working. Dashboards built on names break quietly: the filter still runs, it just matches nothing.
Single and multiple choice answers are stored as choice IDs. Filter and group by the choice itself ("Functional status is Functional") rather than converting to text and matching the label. Text conversion happens after the index is useless, and labels change with translation.
"Contains", "matches regex", and case-insensitive comparisons on free text cannot use an index. They read every row. Use them for one-off exploration in a datagrid, not in a dashboard filter that runs for every viewer. If a text field is filtered often, the fix is upstream: make it a choice question, a site link, or a reference column.
Each arrow in an expression path ("Response > Water point > Community > Region") is another table the database visits. Since mid-2026 the query engine rewrites single-valued paths like this one into efficient joins in aggregate queries, so a well-formed multi-hop expression is much faster than it used to be. Two kinds of path still cost the old price:
Paths that fan out. "Number of water tests of this water point" or "Any response of this site where..." must aggregate a one-to-many set for every row. Use them in a calculated data source, or reverse the query so it starts from the many side and groups by the one side.
Paths that pick the latest. "Most recent E. coli result" sorts the linked records for each row. Precompute it in a calculated data source, as described under "Prepare the data source".
When two widgets need the same joined value, they benefit from expressing the path identically. The query engine merges identical joins within one query, but it cannot merge a path written two different ways.
The Advanced option at the end of a related-survey path lets an expression operate over all responses of a site instead of only the latest. It is the right tool for "how many surveys per team in the last 30 days", as the Advanced expressions guide shows. It is also a full aggregation for every row. Reach for it when the question requires it, and move the result into a calculated data source when the dashboard grows.
With a good data source and good expressions, most widgets are already fast. These habits keep them that way.
Every widget, and the dashboard as a whole, can carry built-in filters. Set them early. A chart that scans one district's responses over the last 12 months costs a fraction of one that scans the whole survey history and then discards most of it visually. Filter on site links, choices, dates, and regions selected from the list, following the guidance under "Write expressions the database can index".
Date filters are especially effective. Most operational dashboards need the current period, not the full history. Use relative date filters ("in the last 90 days", "this year") so the filter stays correct without editing.
Global filters (geographic and Managed by) apply to every site-based widget on the dashboard at once and are the simplest way to scope a large dataset.
A table widget returns at most 1,000 rows. Asking for more does not show more; it only spends time before the cap is applied. Set the widget's row limit to what a viewer will actually scroll, order by an indexed field, and give viewers a datagrid link for the full list. Datagrids page through data on demand and are built for large result sets.
Each column in a table is a separate expression evaluated for every row. Columns that follow long join paths multiply the cost. Show the essentials and keep detail for the row popup, which loads one record at a time.
Pivot tables are the most expensive widget type because each section and each intersecting row and column is another aggregate. Prefer several small pivot tables to one large one with many sections, especially when the sections would otherwise land in different dashboard sections and load only when opened.
Ten widget queries run at once, starting from the top of the page. Everything else waits. A page with 30 widgets triples the wait for the ones at the bottom. If a page needs that many, it needs sections or a console with several dashboards.
The Using dashboards guide recommends keeping each dashboard compact, ideally a single screen. Sections and consoles are how a large project meets that standard without losing content.
A sectioned dashboard renders only the section the viewer is looking at. In Tabs and Sidebar mode, only the active section's widgets run their queries. In Accordion mode, only the expanded sections do. A dashboard with five sections of eight widgets each opens as fast as a dashboard with eight widgets.
Choose sections by theme (Water, Sanitation, Hygiene) or by audience (Summary, Operations, Data quality). Put the section most viewers need first, since it is the one that opens by default. Use Tabs for 3 to 5 sections, Sidebar for more, and Accordion for dashboards read on phones.
Convert an existing dashboard from Settings > Sections > Switch to multiple sections, then split it where the content changes topic using the "Split section here" control between widgets. Everything above the split stays in the current section.
A drilldown opens a focused view filtered to the region, bar, or row the viewer clicked. Its widgets do not run until it opens. A national dashboard with a regional drilldown shows one set of national widgets, then one filtered set of regional widgets, instead of every region at once.
A console combines maps, datagrids, and dashboards as tabs. Each tab loads when opened. When a program has several audiences or several thematic areas that each deserve a full dashboard, put them in a console rather than stacking them on one page.
By default, a saved dashboard shows results up to 8 hours old and the Refresh button in the toolbar forces new queries. Reloading the browser page does not; it reloads the cached results. Tell viewers this.
Dashboard administrators can change the default in Settings > Freshness. The options are Live, 5 minutes, 15 minutes, 1 hour, and 8 hours. Lower values show recent data sooner and make the dashboard slower to open, because more viewers trigger fresh queries. Choose the longest interval the use case tolerates: 8 hours for reporting dashboards, 15 minutes or 1 hour for operations, and Live only for approval queues and similar real-time work where every viewer must see the current state.
Freshness is a dashboard-wide setting. A console tab inherits it from the dashboard it displays. Saving the setting invalidates all existing cached results, so the first load after a change is a full load.
Quick filters and slicers show a list of values, and building that list is itself a query. Base them on choice questions, site types, regions, or reference columns whose values are few and indexed. A quick filter on a free-text question must find every distinct value in the data, and it produces a list no one can use.
When several data sources share a field such as Country or District, merge them into one quick filter, or use the slicer's "Also filter other tables" option, so one selection filters everything. This gives viewers a single control and lets every widget apply an indexed filter.
Map layers are served as vector tiles: the database renders only the area on screen at the current zoom. This is efficient, with two conditions.
Every layer has a minimum and maximum zoom under Zoom level options in its settings. At country level a layer of 50,000 water points is unreadable and expensive. Give the point layer a minimum zoom so it appears when the viewer zooms in, and show a cluster layer, grid layer, or region choropleth at low zoom instead. Cluster and grid layers summarize points into counts, so they render fast at any scale, and a choropleth of counts by administrative region is the most legible national view.
A layer can plot survey responses at the location of their linked site. This works, but at low zoom it makes the database resolve the site location for every response in view. When the goal is to show sites colored by a survey result, start the layer from the site table and pull the survey value through the link, so the geometry comes straight from the indexed site location. Optimizing the survey helps this case as well.
Layer filters follow the same rules as widget filters: region and site type selected from the list, dates relative to today, no text matching. A dashboard's global filters apply to map widgets automatically. Check that a global filter and a layer filter do not contradict each other, which produces an empty map rather than an error.
Photos are the largest data in most surveys, and they cost most in the field, where enumerators upload them over mobile connections.
Set a maximum image size on each Image or Images question. The question's settings in the survey designer offer Maximum Image Size options from 1 megapixel (about 100 KB per image) to 12 megapixels (about 900 KB). Organizations with a custom app configuration can also set a global limit; the app applies the smallest of the question, configuration, and user settings. A 2 megapixel image is sufficient for documenting a water point or a latrine and syncs far faster than a full-resolution photo of up to 4 MB.
In dashboards, an Image widget shows one photo and must be filtered to a single record. A Mosaic widget shows many; filter it to the records that matter and place it in a section or drilldown that opens on demand. For bulk retrieval, use the photo export from the survey rather than a dashboard widget.
Work through these steps in order. Each one removes a layer of cost, and the first two solve most cases.
If a widget still times out after these steps, note the dashboard link and the widget, and contact info@mwater.co. Knowing the steps already tried lets us go straight to the query.