Skip to content
Gravity Tables
minor v4.1.22 + 4.1.28 · · 5 min read

Bulk data in, bulk data out, without the OOM

Two updates shipped a few weeks apart that close the loop on data flow at scale. v4.1.22 brought a CSV import that creates entries via GFAPI in bulk, with case-insensitive header auto-mapping. v4.1.28 rewrote the CSV exporter to stream entries in 500-row chunks so 25,000-row datasets export without blowing the PHP memory limit.

  • CSV import: drop a file, headers auto-map to GF field labels (case-insensitive), each row creates an entry via GFAPI::add_entry
  • Import results summary: created / failed counts, with per-row error messages for the failures
  • Export streaming: 500-row chunks via PHP output buffer, no full dataset in memory
  • 25,000-row exports tested green on shared hosting with 256 MB memory limit
  • Peak-memory logging under WP_DEBUG so you can size your hosting honestly

A pair of versions that, together, fix the way large datasets enter and leave Gravity Tables. Before this work, both halves of the data-flow story had a memory ceiling and a manual seam. After, neither does.

v4.1.22, CSV import for bulk entry creation#

Until this version, getting 500 rows of historical data into a Gravity Form meant either re-submitting the form 500 times or writing a one-off PHP script with GFAPI::add_entry() calls. Most teams gave up on importing the historical data and lived with a partial table.

The new import lives at Forms → [Form Name] → Import in the WordPress admin:

  1. Pick the target form
  2. Upload a CSV
  3. Confirm the auto-mapped headers (or remap)
  4. Click Import

Each row becomes one Gravity Forms entry, written through GFAPI::add_entry() so all the form’s validation rules, conditional logic, and post-creation hooks run as if the row had been a real submission.

Header auto-mapping#

The CSV’s header row is matched to the form’s field labels case-insensitively and with whitespace tolerance. So a CSV header Customer Email matches a form field labelled customer_email or Customer email without manual remapping.

Unmatched headers are surfaced in a confirmation step before import, you can either remap them to a form field via dropdown, mark them as ignored, or cancel and fix the CSV.

Results summary#

After the import runs, you get a results card:

Import complete.
✓ 487 entries created
✗ 13 entries failed

The 13 failures expand into a per-row error list, usually validation failures (missing required field, invalid email format, value out of range). The error messages are the same ones the form would have shown to a user submitting that row interactively.

You can fix the CSV, re-import just the failed rows, and the original 487 stay untouched.

v4.1.28, Streaming exports#

The other half of the data-flow story was already in place, the table’s Export button shipped CSV / Excel / PDF, but the implementation loaded the entire result set into memory, formatted it, then sent it. For most tables that’s fine. For a 25,000-row table on shared hosting with a 256 MB PHP memory limit, it OOMed.

The fix is straightforward in concept and meaningful in practice: stream the result set in chunks.

The new flow#

  1. The Export button POSTs to /wp-json/gt/v1/export
  2. The handler opens an output stream (fopen('php://output', 'w')) instead of building a string in memory
  3. Entries are fetched in 500-row chunks via GFAPI::get_entries() with a paged offset
  4. Each chunk is written to the output stream + buffer-flushed (ob_flush() + flush())
  5. PHP’s memory holds only the current 500-row chunk, never the full dataset

For a 25,000-row export, that’s 50 chunks of 500 rows each, peak memory roughly 4-6 MB instead of 200+ MB.

What this enables#

The tested-and-green threshold went from “about 5,000 rows on standard hosting” to “25,000+ rows on standard hosting”. We test up to 50,000 internally; community reports of 75,000-row exports have come in clean.

For the rare site that genuinely has 100k+ rows in a single Gravity Form, the underlying database query is now the bottleneck, not PHP memory, which is the right place for it to be.

Peak-memory logging#

When WP_DEBUG is on, the exporter logs peak memory consumed during the run:

[gravity-tables] export complete: 24,318 rows, peak 5.2 MB, 1.8s

For sizing decisions (“can our hosting handle the export volume we’ve grown to?”) this is the data point you actually need. You don’t have to guess; you read the log.

What changed (and didn’t)#

The Export button UI is unchanged. The CSV/Excel/PDF format options are unchanged. The role-gating + filter-respecting behaviour is unchanged. What changed is the engine under the hood, same surface, faster + safer behaviour.

For the import, the entry point is new, there was no equivalent feature before. The Import submenu only appears for users with the manage_options capability (admin-only by default) and only on forms where the current user has GF’s import capability.

What’s not in scope (yet)#

These versions ship the 80% case cleanly; the remaining 20% is queued for the 4.3 line:

  • Async exports. For exports that genuinely need 60+ seconds, a job-based pattern (start → poll → download URL) is in nightly. Until then, very large exports may hit PHP max_execution_time limits even though memory is fine.
  • Excel/PDF streaming. CSV streams; XLSX and PDF still buffer because the format requires holding the document tree in memory. For sub-10,000-row Excel/PDF this is fine; beyond that, prefer CSV.
  • Import row-level rate limiting. Hooks downstream of GFAPI::add_entry() can be expensive (e.g. payment notifications). For 5,000+ row imports on a site with heavy notification hooks, you may want to batch with a small delay, currently a manual job, becoming a setting in 4.3.

Upgrade path#

Both features are additive. The export change is transparent, your existing tables export the same way, just without OOMing. The import is a new admin submenu that only appears when a user has the right capability.

If you have custom code that hooks gform_after_submission and runs heavy work, double-check it still completes within reasonable time when invoked 500 times in quick succession. The gravity_tables_entry_created action (shipped in 4.1.31, see roundup) gives you a lighter-weight alternative if the GF hook is too expensive.

#csv#import#export#streaming#performance