---
title: "Limits reference"
description: "Every ceiling a wrun indicator runs under on the chart, in one place: the declaration caps Run checks when it derives the sheet, the runtime ceilings the…"
order: 131
section: "reference"
---

<!-- source: docs/indicators/reference/limits.md; generated by packages/cli/scripts/gen-indicator-docs.ts, do not edit -->

# Limits reference

Every ceiling a wrun indicator runs under on the chart, in one place: the
declaration caps Run checks when it derives the sheet, the runtime
ceilings the browser sandbox enforces on every run, the data bounds, and
the publish bounds. Each row gives the exact number, what it protects, the
shape that hits it, and the message it produces. Most of them are counts
of declarations, because a wrun indicator declares almost everything once.

## Plans

Free and Plus differ in how much you keep on your charts and in your
account:

| | Free | Plus |
| --- | --- | --- |
| Indicators | 3 across your charts | 10 per chart |
| Indicator alerts | 2 | 25 |
| Saved scripts | 100 | 500 |

At the saved-script limit, saving a new script stops with "Saved-script
limit reached (100 on your plan). Delete a script or upgrade to save new
ones."

## Declaration caps

Checked by Run in your browser when it derives the sheet from your
declarations, before anything reaches the chart. A declaration over a cap
is refused in the editor's Console, anchored on the declaration that
breaks it and naming the field (`boxes.0.top`, `renderers.2.size`).

| Limit | Value | What it caps | When you hit it | Refusal |
| --- | ---: | --- | --- | --- |
| Boxes | 16 per indicator | `box(...)` declarations, each drawn once per bar | one declaration per occurrence instead of one gated declaration | `boxes must declare at most 16 entries` |
| Segments | 16 per indicator | `segment(...)` declarations, each drawn once per bar | the same shape | `segments must declare at most 16 entries` |
| Renderers | 64 per indicator | `render.text`, `render.label`, `render.table`, `render.shape`, `render.stats_row`, `render.bgcolor`, `render.barcolor` | a footprint-style file with a text tile per price level | `renderers must declare at most 64 entries` |
| Drawings | 64 per indicator | `draw.line`, `draw.box`, `draw.polyline`, `draw.label` and the widgets, placed from the newest bar | many run-level objects; a growing list is handles instead | `drawings must declare at most 64 entries` |
| Polyline points | 64 pairs | `points` on one declared `draw.polyline` (a polyline handle takes 100,000) | a curve traced point by point in a declaration | `polyline points must be <= 64 pairs` |
| Declared alerts | 64 per indicator; `message` and `description` at most 200 characters each; `title` 1 to 120 characters on one line, without control or invisible characters; `text` names a declared string slot, whose words are cut at 200 characters when the alert fires | `alert(...)` declarations | a signal per level instead of one gate output | `alerts must declare at most 64 entries`, `message must be at most 200 characters`, `alert 'cross' title is 121 characters; it takes at most 120`, `alert 'cross' text names 'ghost', which is not a declared string slot` |
| Handle defaults | `width` 0.5..20, `border_width` 0..10, `opacity` 0..1, `size` and `font_size` 6..64, `line_style` and `border_style` solid/dashed/dotted, `extend` none/left/right/both, `align` left/center/right, `valign` top/middle/bottom, `padding` 0..64, `corner_radius` 0..32, `glow` 0..32, `max_width` 0..2000 (0 = no wrap), `angle` -90..90, `gradient` 2..8 colours, `arrow` none/start/end/both, `axis_label` none/price (text on a label), `panel` overlay/lower; the legacy `borderWidth`, `borderColor` and `lineStyle` spellings still read, never beside their snake_case twins | the defaults one `handles.<kind>(...)` declaration sets for `line`, `box`, `label` or `polyline`, and the same keys on a declared `draw.line`, `draw.box`, `draw.polyline` or `draw.label` | a label handle no longer needs a string slot: `.mark()` draws an emblem with no text | `handles.line.width: width must be <= 20`, `a line has no text; use price` |
| Table geometry | 32 rows x 8 cols | one `render.table`; `cells` must list exactly `rows * cols` slot names; `rows_by` paints the first rows only (a data-only output's value on the table's bar, clamped to 0..`rows`) | a dashboard dump | `table rows must be <= 32`, `table cols must be <= 8`, `table 'stats' declares 2x2 = 4 cells but lists 3`, `render.table 'stats' rows_by names 'v', which is a drawn output; rows_by takes a data-only output (plot none) holding how many rows paint` |
| Table look | `width` and each of `column_widths` 0..4096 px (0 = measured, one per column), `cell_padding` 0..64, `offset` within [-200, 200], `border_width` and `grid_width` 0..10, `corner_radius` 0..32, `font_size` 6..64, `opacity` 0..1, a gradient 2..8 colours, `header_rows` at most `rows`, a `colspan` or `rowspan` inside the grid at every cell carrying the slot, every colour ladder in a pair (`<key>_by` + `<key>s`, or `<key>_packed_by`, never both) | the look keys and `styles` entries on one `render.table` ([Styled tables](../presentation/cards-frames-panels.md#styled-tables)) | a span past the last column, a widths list shorter than the columns | `table 'board' lists 1 column widths for 2 columns`, `table 'board' cell 'b' spans 2 columns from column 2 of 2`, `table 'board' declares text_color_by without text_colors (both or neither)` |
| String slots | 64 per indicator | `string(...)` declarations | one slot per table cell on a big table | `string_slots must declare at most 64 slots` |
| Slot bytes | 1..4096 per slot | `max_bytes` on one slot | a long per-bar readout | `max_bytes must be <= 4096 (the per-slot byte cap)` |
| Frames | 8 per indicator, each at most 96 KiB (98,304 bytes) | `frame(...)` declarations and their `max_bytes` | a snapshot per widget instead of one frame per payload | named by field |
| Mini-chart grid | 1..12 panels of 0..100 candles; labels 1..24 characters, the title at most 40 | the frame of one `draw.minicharts` | a grid of more than 12 markets or timeframes | `a grid holds at most 12 panels`, `a panel holds at most 100 candles` |
| Mini-chart look | `columns` 1..4, `panel_width` 72..320, `panel_height` 48..220, `gap` 0..48, `x` and `y` -2000..2000, `ma_width` 0.5..5 | the options of one `draw.minicharts` | | `columns must be within 1..4` |
| Docked profiles | 4 per indicator; a frame of 1..512 rows; `width_frac` 0.05..0.5 or `width_px` 16..600 (never both); `offset` x 0..4096; `thickness_px` 2..40; `opacity`, `outside_opacity` 0..1; `series` 1..8 with unique names; `gradient` 2..8 colours; `border_width`, `value_area_width`, `poc_width` 0..10 (the POC at least 1); `font_size` 6..64; `step`, `scale_max` above 0; `labels_text` 0..24 characters a row, `tooltips` 0..64, `poc_label_text` 1..24 | `plot.levels(...)` and its frame ([Docked profiles](../presentation/cards-frames-panels.md#docked-profiles)) | a level `beside` itself or on the other dock, a `behind_candles` profile on the lower pane, a `scale: "fixed"` with no `scale_max` | `width_px is refused beside width_frac`, `scale_max needs scale 'fixed'`, `wrun_frame_invalid` for a frame whose series count or shape disagrees with the declaration |
| Level spans | 64 spans per frame, 1..512 prices each, 4096 rows across every span | a `plot.levels` frame under `span: "time"` | one span per bar instead of one per session | `wrun_frame_invalid: <frame>.spans` |
| Panels | 8 per indicator; `series` 1..8 (a table 1..12); frame rows 2000 (a table 128, a pie or tiles 24); `markers` and `guides` 0..8; `summary` 0..5 rows; `height_frac` 0.05..0.9; `title` 1..40, `caption` 1..64; `x_title`, `y_title` 1..40; `width` 0..4096, `column_widths` 0..4096 each (one per column), `cell_padding` 0..64, `offset` -200..200, `font_size` 6..64, `border_width` and `grid_width` 0..10, `background_gradient` 2..8, `palette` 2..8, `bins` 2..200, `columns` 1..8, `slice_gap` 0..8, `trail_width` 0.5..6, a series `width` 0.5..20, marker and guide `width` 0.5..4, labels 1..24 | `panel.<kind>(...)` and its frame ([Frames, panels and compact widgets](../presentation/cards-frames-panels.md#frames-panels-and-compact-widgets)) | a 9th marker, a word on a kind that does not take it, a rich-line word on a boxed line | `markers allows at most 8`, `table allows at most 128 rows`, `series allows at most 12 columns on a table`, `<word> is only valid for <kinds>`, `<word> needs chrome 'grid' or 'none' on a time, category or stacked line` |
| Side strip | `width_px` 160..480, `height_px` 80..800, both with `place: "side"` only; the strip takes at most 40% of the chart and folds away under 480 px | a panel placed beside the chart or a matrix with `dock: "side"` | a side panel on a narrow layout | `width_px is only valid with place: 'side'` |
| Heatmaps | 4 per indicator; `palette` 2..8 colours; `auto_quantile` 0.5..1; `cell_gap` 0..4; `opacity` 0..1; `tooltip` 1..200 characters, `label` 1..40; `out.grid` rows 2..128, and the grid's outputs count toward the 256 | `plot.heatmap(...)` and `out.grid(...)` ([Price canvases](../presentation/price-canvases.md)) | a grid with a row per tick, `cells` beside `grid` | `heatmaps must declare at most 4 entries`, `out.grid 'heat' rows must be between 2 and 128`, `heatmap 'h' takes cells or grid, not both` |
| Profiles | 4 per indicator across footprints, TPOs and time-anchored profiles; `imbalance_ratio` 1.1..20; `stacked_imbalances` 0..10; `value_area` 0.5..0.95; `poc_width` 1..10; `letter_minutes` 1..240; a TPO `palette` 2..26; `width_frac` 0.05..1; `outside_va_opacity`, `background_opacity` 0..1 | `plot.footprint(...)`, `plot.tpo(...)`, `plot.profile(...)` | a footprint over book cells, a TPO colour mode that needs profile cells | `footprint 'fp' does not take period`, `tpo 't' color_mode 'volume' needs cells from a volume_profile input`, `profile 'p' behind_candles needs span 'session' and a filled mode` |
| Matrices | 4 per indicator, `wrun-4` only; a frame of 1..128 prices, 1..12 columns and at most 1536 cells; cell text and column labels 1..16 characters; `column_width` 24..160; `row_max_px` 8..64; `cell_padding` 0..12; `tooltip` 1..200, `label` 1..40 | `plot.matrix(...)` and its frame | a matrix on a `wrun-3` sheet, a 13th column | `matrices needs abi_version "wrun-4" (wrun-3 has no frame channel)`, `wrun_frame_invalid: <frame>.cells` |
| Widgets | 8 cards per indicator and 32 cards, feeds and meters per pane; a card's `rows` 1..12 (0..12 with a headline); `title` 1..40 (80 with a template), a row `label` 1..24 (64 with a template), a literal value 0..64; `offset` -4096..4096; `width` 80..1200; `corner_radius` 0..32; `padding` 0..24; `border_width` 0..10; `font_size` and `title_font_size` 6..64; `background_gradient` 2..8; `state_colors` exactly 4; a ladder frame 1..256 rows, `width_frac` 0.02..0.5, `offset` x 0..4096 with y 0; a feed 1..50 lines of 1..80 characters; a meter `ramp` 2..5, `bar_height` 2..40 | `draw.card`, `draw.feed`, `draw.meter`, `draw.ladder` ([Status cards](../presentation/cards-frames-panels.md#status-cards)) | a card with neither rows nor a headline, `accent_color` beside `state_colors` | `card 'x' needs a row or a headline`, `title takes 1 to 40 characters, 80 with a {{template}}`, `offset entries must be integers from -4096 to 4096`, `accent_color and state_colors are exclusive` |
| HUD cards | `width` 80..1200 (300, or 180 at one column); `offset` -4096..4096; `corner_radius` 0..32; `padding` 0..24; `border_width` 0..10; `text_glow` 0..8; `accent_colors` 2..8; one headline tile per card; `rings` 1..3; a tile `font_size` 6..64, a tile `height` 16..120 | `render.hud(...)` and its tiles ([HUD cards](../presentation/hud-and-hover-cards.md#hud-cards)) | two headline tiles, a fourth ring | `a HUD takes at most one headline tile`, `look must be one of 'default', 'glass', ...` |
| Number format | `decimals` 0..8; `unit` at most 8 characters on panes, panels, levels, cards, ladders and canvases; `decimals` and `signed` only beside `format` (an output's `unit` stays unbounded with or without `format`, as it always was; without `format` it is never printed) | every `format` word (outputs, panes, panels, levels, cards, ladders, canvases) | a unit longer than 8 characters on a pane, panel, level, card, ladder or canvas | `decimals must be an integer from 0 to 8`, `unit takes at most 8 characters`, `decimals needs format` |
| Text size | 6..64 px | `size` on `text` and `label` renderers, and every `font_size` | | `size must be >= 6`, `size must be <= 64`, `font_size must be an integer from 6 to 64` |
| Type words | `font_family` ui/mono/serif/rounded, `font_weight` normal/medium/bold, `valign` top/middle/bottom, `gradient_direction` vertical/horizontal, `corner_radius` 0..32 | every surface that takes them (panels, levels, cards, feeds, meters, ladders, HUDs, handles, drawings, renderers, canvases) | a CSS family name, a numeric weight | `font_family must be one of 'ui', 'mono', 'serif', 'rounded'`, `font_weight must be one of 'normal', 'medium', 'bold'`, `corner_radius must be between 0 and 32` |
| Display offset | -500..500 bars, integer | `displacement_bars` on an output | a lagging span longer than 500 bars | `displacement_bars must be >= -500`, `displacement_bars must be <= 500`, `displacement_bars must be an integer count of bars` |
| Shape offset | -500..500 bars, a whole or fractional literal | `from` / `to` on a box or segment | a zone anchored further back than 500 bars | `a literal bar offset must be within -500..500 bars of the current bar` |
| Width ladder | 1..10 entries, each 0.5..20 (0.05..20 on bar and histogram) | `widths` beside `width_by` | | `widths must list at most 10 entries`, `widths entries must be <= 20` |
| Line width | 0.5..20 | `width` on an output or a segment | | `width must be <= 20` |
| Border width | 0..10 | `borderWidth` on a box | | `border_width must be <= 10` |
| Edge width | 1..10, integer | `edge_width` on a range | | `edge_width must be <= 10` |
| Gradients | 2..8 colors | `gradient` on a range, a line or a docked profile, `fill_gradient` on an area, `background_gradient` on a widget, a HUD or a table, a box's `gradient` | | named by field, `<key> must list 2 to 8 colours` |
| Opacity | 0..1 | `opacity` on an output or a box (box default 0.2) | | named by field |
| Color string | 1..64 chars, or a theme token | any `color` on a box, segment, renderer, or drawing; `theme.up`, `theme.down`, `theme.text`, `theme.muted`, `theme.bg`, `theme.grid` and `theme.accent` pass everywhere a colour goes (a colour param's default and presets excepted), and any other `theme.` word is refused | a `theme.` word outside the seven | `color must be a non-empty string of at most 64 chars`, `colour token must be one of theme.up, theme.down, theme.text, theme.muted, theme.bg, theme.grid, theme.accent`, `a colour param takes #rrggbb or #rrggbbaa; put the token on the colour key` |
| Colour word | `#rrggbb`, `#rrggbbaa` or a theme token | every colour key this round added (panels, levels, cards, feeds, meters, ladders, HUDs, canvases, and the frames they read) | `rgba(...)` or a named colour on a panel series | `a colour must be #rrggbb, #rrggbbaa or one of theme.up, theme.down, theme.text, theme.muted, theme.bg, theme.grid, theme.accent` |
| Box fill color | hex, `rgb()`, or `hsl()` | a box `color` (the fill takes the opacity) | a named color such as `"red"` | `color must be a hex, rgb() or hsl() color (the fill takes the opacity)` |
| Palette | at least 2 entries | `colors` beside `color_by` | | `color_by needs 'colors' with at least 2 entries` |
| Celled cap | required, positive integer, counts TUPLES | `max_cells` on a celled input | a celled input declared without it | `celled input 'profile' needs max_cells, e.g. input("profile", volume_profile.cells, { max_cells: 512 }) ...` |
| Bucket size from a setting | a `param.int` whose `min..max` lies inside 1..500 | `ticks_per_bar: "@<param>"` on a `volume_profile.cells` input | an open-ended setting, or one that reaches past 500 | `input 'profile' ticks_per_bar references "@ticks", whose min..max (open..open) must lie inside ticks_per_bar's range 1..500` |
| Colour alpha | a decimal from 0 to 1 after the setting's name, `"@<param>/0.4"` | every colour bound to a setting: the default lands as `rgba()` where the colour takes it, as `#rrggbbaa` where it takes a colour word | a panel series colour, which binds without an alpha | `whose alpha is not a decimal from 0 to 1 ("@bull/0.4")`, `a panel series colour binds without an alpha` |
| Outputs | 1..256 | output declarations; the sandbox tracks 256 output slots per run (a file whose visuals are all frames may declare none) | a file past 256 outputs, or a raw write past the tracked slots | `WRUN output index 300 out of bounds for 256 tracked outputs` |
| Settings | 128 per indicator, after expansion: most kinds count 1, a `unit` list on a number 1 more, a `range` 2, a `session` 3, a `list` its `max` + 1, a `draw.minicharts` grid that binds no setting 9 to 16 (its look's own settings) | `param.<kind>` declarations and each hidden `market.*` setting; OpenMarket's cloud accepts at most 128 settings per indicator, so the build refuses the declaration that crosses the line ([Picks, lanes, the cap](../settings/picks-and-lanes.md)) | a list whose `max` alone crosses 128 (a list's max counts its slots and the count, so `max` is at most 127), or many composites | `the sheet would carry 129 params after expansion (a range derives two, a list max + 1, a session three, a unit list one more); hosted lanes cap params at 128` |
| Text settings | `max_bytes` 1..4096 UTF-8 bytes per setting, 256 when left out; the default within it and free of invisible and control characters | `param.text` / `param.text_area` declarations ([Setting kinds](../settings/kinds.md)) | a default longer than its cap, a pasted name carrying a bidi control | `param.text 'label' default is 12 UTF-8 bytes, over its max_bytes 4`, `param.text 'label' default carries a newline, a control or an invisible character` |

## What the chart trims when it draws

These limits never refuse a run. The chart applies them when it draws, so one indicator can't bury another, hide the chart's own buttons, or draw more rows than a screen can show.

| Limit | Value | What happens past it |
| --- | --- | --- |
| Drawing and label text | 256 characters | the text is cut and ends with an ellipsis |
| Drawing order | `zorder` -100..100 | a higher value draws as 100, a lower one as -100 |
| Drawings per run | 2,000, counting every bar a segment draws, every declared drawing and every live handle | the newest 2,000 are drawn and the oldest are left out; the indicator's legend row says "Drawings capped", and its card says how many of the total were drawn |
| Widgets | 16 cards, feeds, meters and ladders per indicator per pane | the ones past 16 are not drawn |
| Legend entries | 32 per indicator, each 64 characters | entries past 32 are not shown; longer text ends with an ellipsis |
| HUD cards | inside the price pane: below the symbol row and the legend, left of the price axis | an `offset` that would move a card out is clamped; a card under 0.15 opacity lets clicks through |
| Card controls | `controls: "none"` | the glyphs stay hidden at rest; hovering always shows hide and collapse |
| TPO rows per period | 512 per day, week or month | the chart doubles the row height until every period fits, at most 6 times, for `"auto"` and a number alike; every run says so with a warning row in the Console and the warning on the legend chip: `tpo 'tpo': row_height 1 coarsened to 8 (512 rows per day is the chart's limit)` ([Row height](../presentation/price-canvases.md#row-height)) |
| TPO rows on screen | 3 px | thinner rows merge on the price grid: a merged row's letters are the union of its rows' letters, its counts and volumes add up, and the POC and the value area are computed on the merged rows |
| TPO blocks | 262,144 per TPO | the blocks past it are not drawn, and the Console says so |
| Heatmap cells per indicator | the chart's budget, across all of an indicator's heatmaps, the loaded history and the live bars together (one cell per book level, grid row or profile bucket on each bar) | the oldest bars are dropped and the newest are drawn whole; the indicator's legend row says "Cells capped", and its card says how many bars are drawn |

Names share one namespace across outputs, boxes, segments, renderers, and
drawings; `color_by`, `width_by`, and `shape_where` cannot name their own
output; each ladder half without the other is refused by name. The full
validation list is at the end of
[Declarations and the sheet](declarations.md#validation-in-one-list).

## Runtime ceilings

Enforced on every run by the sandbox your browser runs the module in. A
breach refuses the WHOLE run by name and the editor's Console says why;
nothing is truncated, clamped, or silently dropped. An indicator that
already drew keeps its last good render, and its legend offers a retry.

| Limit | Value | What it protects | When you hit it | Refusal |
| --- | ---: | --- | --- | --- |
| Module memory | 4 MiB (4,194,304 bytes), fixed | the linear memory a module uses | a buffer sized far past any param's `max` | `WRUN memory 8388608 bytes exceeds limit 4194304 bytes` |
| No growth after `onStart()` | 0 bytes of growth | flat per-bar memory across a long history and the forming bar's replays | an array that grows per bar, string concatenation in `onBar()` | "The Indicator allocated memory after init() (65536 to 131072 bytes): the sandbox forbids growth once the bars start." |
| Output values | finite or NaN | a chart and an alert that mean something | a division by zero written as `Infinity` | `WRUN output 0 is Infinity; guard the calculation and emit NaN when the value is undefined` |
| Run deadline | 20 s per run (the compile has its own 15 s) | one runaway module never holds the sandbox: the worker is stopped and replaced | unbounded loops, deep recursion | "The run did not finish within 20 s." |
| Cell block | `max_cells` tuples per bar, per celled input | the one buffer of `max_cells` tuples the build allocates per celled input when the module starts; each bar's block is read into it once, and `in_<input>_view()` reads it in place | a deeper book than the cap; a profile with more price buckets than the cap | `... input 1 bar at ts 1725580800000 has 1200 cells; max_cells is 1000, so the evaluation is refused (a block is never truncated)`; a volume profile declared `missing: "empty"` is not refused: that bar reads an empty block (`in_<input>_cells()` is `0`) and the run goes on |
| TPO rows per bar | 4,096 | the rows one bar expands into at a TPO's row height | a declared `row_height` far below one bar's range (`"auto"` stays under it) | `tpo 'tpo': row_height 0.05 expands one bar into 5001 price rows, above the 4096-row limit (declare a larger row_height)` |
| Cell read bounds | inside exported memory | a copy that would overrun the module's memory | a pointer outside the preallocated buffer | `WRUN wrun_arg_bytes(1, 65536): 1536 bytes do not fit the module's exported memory of 65536 bytes; reserve max_cells * 8 bytes per celled input and pass a pointer inside that buffer` |
| Cell phase | `onBar()` | the cell readers answer for the bar being evaluated; before the first bar (in `onStart()`, at module start) the count reads `-1` and there is no block | the host's cell import called in another phase, which the generated readers never do | `WRUN wrun_arg_len is callable during state() only (called during finalize); read cell blocks inside state() and carry what finalize() needs in module state` |
| String phase | `onBar()` | slots belong to the row being committed | a string sent outside `onBar()` | `WRUN wrun_output_str is callable during finalize() only (called during state); ...` |
| String slot bytes | `max_bytes` per slot (at most 4096) | the buffer reserved per slot | a line longer than the slot | `WRUN string slot 0 write of 40 bytes exceeds the slot's max_bytes 32; strings are never truncated; ...` |
| String bytes per row | 65,536 (64 KiB) | one bar's text across all slots | many slots near their cap on one bar | `WRUN string writes for one row total 70000 bytes, over the per-row limit 65536; emit less text per bar` |
| Strings and frames per run | 2,097,152 (2 MiB), together; a live session counts as one run | one run's text and frame snapshots across all bars | long readouts on every bar of a long history | `WRUN string and frame writes for this run total 2100000 bytes, over the per-run limit 2097152; emit less text or fewer rows` |
| UTF-8 | valid sequences only | a slot never carries a replacement character silently | raw bytes sent through the host import | `WRUN string slot 0 write of 5 bytes is not valid UTF-8; wrun_output_str carries UTF-8 text only (encode before writing)` |
| Expanded render result | 8 MiB (8,388,608 bytes) | the selection the chart receives after the run: 16 bytes per selected entry, 8 per carried number, the UTF-8 bytes of every carried string (table cells charge 8 more per cell; handles and a strategy's result count too) | many renderers over a long history with long strings; hundreds of live polylines | `WRUN render result exceeds 8388608 bytes expanded (wrun_render_result_too_large): fewer rows, shorter strings, or fewer renderers/drawings` |
| Live handles per kind | 500 | the handles of one kind alive at once, counted on every creation | a zone tracker that never deletes | `wrun_draw_kind_limit`: `500 live box handles already exist; the cap is 500 per kind ...` |
| Live handles in total | 1500 | every kind together | the same shape across kinds | `wrun_draw_total_limit`: `1500 live handles already exist; the cap is 1500 in total ...` |
| Polyline handle points | 1..100,000 per call | the points one `setPoints` sends; the buffer must fit the module's memory | a path that never trims its oldest points | `wrun_draw_polyline_too_large`, `wrun_draw_polyline_empty`, `wrun_draw_polyline_out_of_bounds` |
| Polyline points per run | 524,288 across the live polylines | the points every live polyline holds, checked on each `setPoints`: a re-sent path gives its old points back first and a deleted one frees its own | hundreds of long paths kept alive at once | `wrun_draw_polyline_budget` |
| Draw calls per bar | 4096 | every handle call on one bar | re-sending every live handle on every bar | `wrun_draw_calls_per_row`: `more than 4096 draw calls on one row ...` |
| Draw phase | `onBar()` | handle ops belong to the row being committed | a handle operation outside `onBar()` | `wrun_draw_phase` |
| Handle identity | ids `>= 0`, one space across kinds, kind declared in `handles`, coordinates finite | the ledger the chart mirrors, kind by kind | a negative id; a label on a box's id; a kind the file does not declare; a `NaN` corner; a setter on an id nobody holds | `wrun_draw_id_negative`, `wrun_draw_kind_mismatch`, `wrun_draw_kind_undeclared`, `wrun_draw_non_finite`, `wrun_draw_handle_missing` |
| Handle style values | the prop's range, integer where required, legal on the kind | the same setter table the engine enforces | `size` on a line; `width` of 30; `style` of `7` | `wrun_draw_prop_unsupported`, `wrun_draw_style_out_of_range`, `wrun_draw_prop_unknown` |
| Handle label text | a declared slot, written on the bar of the draw call | the text a label carries is the slot's bytes at call time | `text(...)` before the slot's sender ran that bar | `wrun_draw_label_slot_absent`, `wrun_draw_label_slot_undeclared` |

The module contract is checked by Run before any of this: the four
engine functions the build adds around `onBar()` with exact signatures (a
file that exports them itself is checked the same way,
[The four-function form](../core-concepts/execution-model.md#the-four-function-form)),
exported `memory` when the cell, string, or polyline channel is imported,
every present draw import with its exact signature, and an import set
limited to the contract's allowlist. Before
a module runs, the chart checks its bytes against the sheet's
`wasm_sha256` and refuses a mismatch. Those messages are in
[Common errors](../faq/common-errors.md).

## Data bounds

| Limit | Value | Where |
| --- | ---: | --- |
| History | the bars the chart has loaded | every source is fetched over the chart's loaded window, and a pan back runs again over the extended window; warm-up is the file's decision: an output `onBar()` leaves unwritten is NaN and draws nothing |
| Live updates | the forming bar, about once a second at most | ticks in between are coalesced and the last one is never dropped; only the forming bar is replayed; a live gap wider than 250 bars rebuilds the run |
| Source deadline | 45 s per source | a source that does not answer in time fails the run with its name |
| Empty source | refused | a required source that answers empty fails the run: "<Label> data is unavailable for '<title>' (the <type> source lane answered empty or was declined), so the indicator cannot compute." |
| Coarse pins | as of the coarser candle's close | a pinned `interval` contributes to a row only once its candle has closed; that market's candles are fetched from two leg spans before the chart's first bar |
| Pinned intervals | coarser than the chart's and a whole multiple of it (`WEEK` counts 7 days) | any other interval pin is refused by name before any fetch |
| Pinned venues | crypto, `POLYGON`, `FX_OTC` | stocks and ETFs on `POLYGON`, forex, gold and silver on `FX_OTC` are served like a crypto pin; an index pin (`POLYGON_INDICES`) reads its `missing` fill with a warning row; a CME Group pin is refused by name ([Multi-source](../core-concepts/multi-source.md#stocks-forex-and-gold)) |
| Multi-day pins on session venues | `3d` refused | a `3d` candle on a stock, forex or gold market groups trading days, so the pin is refused before any fetch (`wrun_pin_session_interval_unserved`); pin `1d` or `1w` |
| Intrabar | finer than the chart's interval and dividing it evenly, `1m` at the finest | so the chart must be 2 minutes or coarser; `max_cells` at least the finer bars in one chart bar; closed finer bars only |
| Book depth | at most 500 levels each side | the chart's own order book at its own grouping, whatever `block_size` and `max_depth` say, so `max_cells: 1000` covers any bar |
| Options chain | the live row only | the chain arrives on the last row; size `max_cells` for the chain (a BTC chain is about 1,550 contracts) |
| Cell memory, per celled input | `max_cells` x tuple width x 8 bytes | 4 f64 per `volume_profile` tuple, 3 per `book` tuple, 6 per `intrabar` bar, 10 per `options_chain` contract, all inside the 4 MiB of module memory; the build allocates this buffer once per celled input when the module starts, reads each bar's block into it once, and `in_<input>_view()` reads it in place (a copy through `in_<input>_read(ptr)` holds the block twice) |
| Alerts | 600 bars of the chart's interval, at most | the window is the larger of the file's `warmup()` and its largest setting maximum (up to 500); an indicator that reads a coarser pin gets all 600 unless it also reads `book` or `volume_profile` cells, and those bars hold 600 / (leg / chart) candles of the pin (a 4h pin: 150 on 1h, 37 on 15m, 12 on 5m), so a longer higher-timeframe average arms but stays empty; another market's candles on a secondary `ohlcv` input and a coarser `1m` to `1w` pin that is a whole multiple of the chart's interval are served; a pin on the first input, a finer pin, a custom timeframe and a source alerts cannot evaluate are refused when you save the alert ([Alerts](../functions/alerts.md)) |
| Strategy order ids | 64 distinct per run (`closeAll` counts as one) | the 65th distinct id refuses the run (`wrun_strategy_id_limit`); an id is a slot, re-issuing one replaces its pending order |
| Strategy id text | 64 bytes of UTF-8 per id, `from` name or OCA name | longer text refuses by name (`wrun_strategy_id_too_long`), never truncated; invalid UTF-8 refuses (`wrun_strategy_id_invalid_utf8`) |
| Strategy calls per bar | 4096 order calls and getter reads per bar | over it refuses (`wrun_strategy_calls_per_row`); an infinite price refuses (`wrun_strategy_non_finite`), NaN is absent |
| Closed trades per run | 10000 | the 10001st closed trade refuses on the bar it closes (`wrun_strategy_trade_limit`); every engine rejection (pyramiding, legs, sizing, margin, the forming bar) is counted in `rejectedOrders`, never a refusal |
| Strategy result bytes | inside the 8 MiB expanded render result | the trades, orders and equity together with the drawings (`wrun_render_result_too_large`) |
| Strategy feed | the price input reads `ohlcv` on the chart's own market, no symbol or exchange pin | refused by name (`wrun_strategy_feed_not_ohlcv`); `slippageModel: "bookEstimate"` refused (`wrun_strategy_slippage_model_unsupported`); `strategy.position` and `strategy.equity` are reserved output names (`wrun_strategy_reserved_output`) |

## Publish bounds

| Limit | Value |
| --- | ---: |
| Open source code | 256 KB ("The source is {size} KB; the limit is {limit} KB.") |
| What changed | one line of up to 280 characters, from the second version |
| Name | lowercase letters, digits, and dashes after your `@yourname/` scope; fixed after the first publish |

The module runs sandboxed: no filesystem, no network, no order
capability. Each wrun indicator on a chart, a draft from Run included, counts
against the chart's indicator limit. [Publishing](../functions/publishing.md)
has the dialog.

## Other bounds

Bounds that are not a number in the tables above.

| Shape | In wrun |
| --- | --- |
| Sources per indicator | No per-indicator source budget. On the chart, a wrun indicator counts against your plan's per-chart indicator limit once per distinct source it reads ([Data sources](../core-concepts/data-sources.md)). Each source is fetched once per run over the chart's loaded bars, however many inputs read it; a pinned market or interval on `ohlcv` adds its own fetch, a `funding` or `oi` interval pin adds none. |
| Output objects per run | No object count. Every output is one number per bar, in at most 256 slots; decorations are declared once. Every bar emits a row, with NaN in each output `onBar()` did not write, so the leading NaN rows of a warm-up count toward the per-run budgets above. What reaches the chart is capped by the 8 MiB expanded render result, and past 2,000 drawings in a run the chart draws the newest 2,000. |
| Table cells | 32 x 8 = 256 cells per table renderer, each a string slot; 64 slots per indicator. A `panel.table` over a frame takes 128 rows by 12 columns instead, and a `plot.matrix` 128 prices by 12 columns within 1536 cells. |
| Collection size | No element cap. Collections live inside the 4 MiB module memory and are allocated before the first bar, at module start or in `onStart()`. |
| Nesting and recursion depth | The compiler's own rules apply; the module's stack lives inside its memory, and the 20-second run deadline stops runaway recursion. |
| Backtest bars and runs per day | No grant to spend: the chart's Strategy Tester runs a wrun strategy in your browser over the bars the chart has loaded and recomputes as you pan ("This strategy runs locally over the chart window and recomputes as you pan."), [Reading the Strategy Tester](../strategies/reading-the-tester.md). |
| Strategy declaration bounds (`leverage`, fees, funding) | Checked by Run with the engine's rules: `initialCapital` and `leverage` more than 0, `pyramiding` an integer of at least 1, `maintenanceMarginPercent` at least 0 and under 100, the fee and slippage rates at least 0, enum fields the engine's literals. A setting linked to a param is checked once before the first bar and falls back to the default when out of range, reported. [Writing strategies](../strategies/writing-strategies.md) has the table. |

## Notes that save debugging time

- **`max_cells` is a contract, not a hint.** The build sizes one buffer
  per celled input from it (`in_<input>_capacity` f64s, tuples x width)
  when the module starts, and `in_<input>_view()` reads it in place; size
  a `book` input for the chart's depth: at most 500 levels a side, so 1000
  tuples.
- **Strings never truncate.** Pick `max_bytes` for the longest line the
  slot will ever carry; the generated builder counts the bytes a line
  REQUIRES, so an oversize line refuses by name instead of clipping.
- **Most caps are counts of declarations.** You meet them when you press
  **Run**, in the editor's Console, anchored on the declaration. A
  repeating shape is one declaration gated per bar with `when`, never one
  declaration per occurrence.
- **Segments count as drawings.** A segment drawn on every bar of a long
  window is the shape that reaches 2,000 drawings per run; gate it with
  `when` or narrow its span.
- **The render cap counts expanded bytes.** Sixty-four text renderers
  over a long history with long strings is the shape that reaches 8 MiB;
  leave slots unwritten on quiet bars so those rows select nothing.
- **Delete what you no longer draw.** A handle stays alive until
  `delete()`; the per-kind cap is met by the tracker that creates a zone
  on every pivot and never frees one. Keep a bounded pool of ids and
  delete the oldest before reusing its id, and re-send only the handles
  that changed on a bar (the calls-per-bar cap counts every setter).
- **Allocate once.** Buffers are sized at module start or in `onStart()`
  from a param's `max`; per-bar allocation is the one pattern that both
  slows a long history and trips the no-growth rule.

## Example: the caps as contracts

A book-imbalance indicator that sizes everything from what the chart
delivers: the chart's order book carries at most 500 levels a side, so
`max_cells` is 1000; the cell buffer is the build's own, sized from that
cap and read in place; and the readout slot is sized for its longest
line:

```typescript sample=limits-contract
input("close", ohlcv.close);
// The chart's order book carries at most 500 levels a side, so a bar never holds more than 1000 tuples:
// max_cells is a contract, and a bigger block would refuse the whole run.
// block_size is required by the declaration; the chart keeps its own price grouping.
input("book", book.cells, { max_cells: 1000, block_size: 10 });
output("imbalance", line, lower, { unit: "ratio" });
// The longest line is "500 bids / 500 asks" (19 bytes); 32 leaves room and stays under the 4096 cap.
string("depth", { max_bytes: 32 });
render.text("depth_mark", { y: "imbalance", text: "depth", size: 10 });

function onBar(): void {
  const n = in_book_cells();
  if (n <= 0) return;
  // The build's own buffer, sized once at module start: max_cells x 3 cells per [price, size, side] tuple.
  const cells = in_book_view();
  let bidSize = 0.0;
  let askSize = 0.0;
  let bids: i32 = 0;
  let asks: i32 = 0;
  for (let i = 0; i + 2 < n; i += 3) {
    if (cells[i + 2] > 0.0) {
      bidSize += cells[i + 1];
      bids += 1;
    } else {
      askSize += cells[i + 1];
      asks += 1;
    }
  }
  const total = bidSize + askSize;
  const imbalance = total > 0.0 ? (bidSize - askSize) / total : NaN;
  if (isNaN(imbalance)) return;
  out_imbalance(imbalance);
  sb_clear();
  sb_int(bids);
  sb_text(" bids / ");
  sb_int(asks);
  sb_text(" asks");
  str_depth_sb();
}
```

The scan branches on each tuple's `side`, never on its position: the chart
lists the bids best first, then the asks from the farthest to the best.
