Choose server pushdown or bounded browser execution

Use this walkthrough when the same map workflow may begin with a Honua feature layer or a direct GeoParquet object. Make the execution boundary and ceilings visible before bytes move, then compare measured evidence rather than assuming the server or browser is cheaper.

Outcome

Lane Work pushed down Browser work Evidence boundary
Honua Arrow query columns, filter, bbox, ordering, limit bounded Arrow IPC and WKB decode, map/table handoff exact response rows, bytes, batches, elapsed time, and peak backing memory
Direct GeoParquet object and row-group selection where the engine supports it bounded DuckDB-WASM scan, Arrow batches, progressive map render observed HTTP ranges/bytes plus result rows and memory ceiling; rows scanned and row groups pruned remain unverified

The runnable Map a bounded Arrow result produces this deterministic fixture outcome:

execution             server-pushdown
rows / batches        1 / 1
admitted payload      4,160 bytes
row ceiling           25
transfer ceiling      16,384 bytes
backing ceiling       65,536 bytes
cancel outcome        no batch admitted; next run succeeds

The complete Overture Columnar Lab project applies the browser lane to a pinned 656,568,610-byte public object without permitting full-object HTTP fallback.

1. Define hard budgets before opening the source

import { createApacheArrowResponseDecoder, openColumnarSession } from "@honua/sdk-js/columnar-workflow";

const session = openColumnarSession({
  kind: "honua-feature-query",
  id: "bounded-parcels",
  baseUrl: "https://example.invalid/",
  serviceId: "Parcels",
  layerId: 0,
  format: "arrow",
  sourceVersion: "deployment-v1",
  schemaVersion: "layer-v1",
  authorizationScope: "user",
}, {
  decodeServerResponse: createApacheArrowResponseDecoder({
    geometryKind: "point",
    importModule: () => import("apache-arrow"),
  }),
  budgets: {
    maxRows: 25_000,
    maxBatches: 32,
    maxTransferBytes: 16 * 1024 * 1024,
    maxBackingBytes: 32 * 1024 * 1024,
  },
});

These are enforced ceilings, not telemetry labels. A response stops before emission when any ceiling is crossed.

2. Plan a server-pushdown subset

const plan = session.plan({
  columns: ["name", "created"],
  bbox: [-158.1, 21.2, -157.6, 21.8],
  filter: {
    kind: "comparison",
    operator: "gte",
    left: { kind: "property", name: "objectid" },
    right: { kind: "literal", value: 1 },
  },
  orderBy: [{ field: "created", direction: "desc" }],
  limit: 5_000,
});
console.log(plan.execution, plan.pushdown, plan.boundedBy, plan.request);

Expected outcome: execution is server-pushdown, and the URL or POST body contains the bounded query. Planning alone does not prove a deployment serves Arrow or Parquet. Check its capability manifest before execution.

3. Stream, map, and cancel with backpressure

Install Apache Arrow as the optional peer and pass createApacheArrowResponseDecoder() only when the deployment advertises Arrow IPC. In browser builds, inject a literal () => import("apache-arrow") so the bundler can resolve the optional peer; Node applications may use the default loader. Each loop requests the next decoded batch only after the handler completes.

for await (const { batch, evidence } of session.stream({
  columns: ["name", "created"],
  bbox: [-158.1, 21.2, -157.6, 21.8],
  limit: 5_000,
  signal: controller.signal,
})) {
  const rows = session.table(batch, 5_000).rows;
  mapBatch(rows);
  console.table(evidence);
}

Expected outcome: evidence reports cumulative rows, batches, admitted payload bytes, elapsed time, peak backing bytes, and governing ceilings. Cancellation, an exceeded ceiling, or an unsupported layout is a typed ColumnarWorkflowError, never partial success.

The built-in bridge decodes a bounded GeoArrow 0.2 WKB subset: Binary/LargeBinary Point, LineString, or Polygon in XY/XYZ, plus one object-id, one UTF-8/dictionary field, and one timestamp field. It ignores embedded EWKB SRIDs, preserves validated optional column-level crs/crs_type metadata, and reads the geometry declaration from schema-level GeoParquet metadata when no row can establish it. It never invents a CRS84 default. BinaryView, multi-geometries, GeometryCollection, M/ZM coordinates, ambiguous or additional fields, and Parquet responses require an application decoder and fail closed otherwise.

The checked-in interoperability fixture and its exact producer receipt were emitted by reviewed Honua Server head 66a9d34496c6f6a03dd571957062f773bfef7f0a, merged as 4ef53ce7f49b78aad3572db1dfc3be88a6654a43. It is fixture evidence, not a live deployment claim.

4. Switch to direct GeoParquet for browser analysis

Use the same bounded query shape with a direct-geoparquet source. DuckDB-WASM performs projection, filtering, bbox selection, and limit in the browser. Inspect metadata first rather than guessing from a suffix.

const direct = openColumnarSession({
  kind: "direct-geoparquet",
  id: "parcel-object",
  url: fixtureUrl,
  sourceVersion: fixtureEtag,
  schemaVersion: "geo-1.1",
  authorizationScope: "public",
});
console.log(await direct.inspect());

Expected outcome: execution is browser-bounded. A bbox query requires an explicitly declared WGS84 longitude/latitude CRS. Use server pushdown when it materially reduces bytes; use direct browser execution when the object itself is the product boundary.

The Overture project adds the production controls needed for a large object: a one-square-degree AOI maximum, five projected columns, a 200-row limit, a 256 MiB DuckDB ceiling, a 1 MiB JavaScript result ceiling, source and engine deadlines, range-only HTTP, a three-entry versioned cache, progressive rendering, and runtime termination on cancellation. Scheduled evidence rejects every un-ranged GET. DuckDB does not expose actual rows scanned or row groups pruned, so the project does not claim those metrics.

5. Pick an explicit handoff

Use table for a small decoded preview, worker for a transferable batch descriptor, render for a deck.gl-oriented zero-copy descriptor, or download to preserve the exact bounded server request. Worker operation names are application-owned, so worker(batch, "projection") can address a registered operation without pretending the handoff itself executes it.

Troubleshooting