Skip to content
ChartDeck ChartScript docs Account Open the terminal

Performance

The limits a ChartScript run works within, and how to write loops, drawings and requests that stay fast on every chart and every plan.

Your script runs on every bar of its run when it loads (5,000 bars unless it sets calc_bars_count), and again on the newest bar each time the price moves. A script that is fast on 500 bars can be slow on 20,000, and a calculation that takes a millisecond per update is fine until it runs on several charts at once. This page lists the limits ChartScript enforces, explains why they exist, and shows the patterns that keep a script well inside them.

#How a script spends its time

There are two kinds of run:

  • The full run (also called the history run) goes through every bar of the script's run, from the oldest to the newest. Scrolling the chart back never adds bars to it. It happens when you add the script, change an input, change the chart's market or timeframe, or the script is reloaded.
  • Live updates run the script again on the newest bar only, as new prices arrive. When a bar closes, the closing run fixes that bar's values for good and the next bar starts.

Your script runs in a background worker, separate from the chart itself, so a slow script does not freeze the chart. It does make its own lines arrive late, and past the limits below it is stopped.

#Limits at a glance

#By plan

LimitBeginnerPlusProElite
Bars per run5,0007,50010,00020,000
Script storage (all saved versions, compressed)2 MB100 MB500 MB2 GB
Extra timeframes and markets per script204080128
Scripts you can keep31550250
Versions kept per script550200Unlimited

On a phone, a script runs on at most 2,000 bars (or your plan's number, if lower), and its full run gets twice the time.

#The same on every plan

LimitValue
Plots128 per script
Inputs400 per script
Alert conditions200 per script
Labels, lines, boxesup to 1,000 each (max_labels_count, max_lines_count, max_boxes_count; default 50)
Polylinesup to 500 (max_polylines_count; default 50)
Tables50 per script, 2,000 cells per table
History for dynamic offsetsmax_bars_back 1 to 10,000 (default 500)
Full run2,500 ms (5,000 ms on a phone)
One live update50 ms
One loop on one bar100,000 passes
All loops on one bar1,000,000 passes
Array or matrix100,000 elements
Map50,000 entries
String40,960 characters
Log messages200 per run

For drawings, asking for more than the maximum quietly gives you the maximum. When a script makes more drawings of one kind than its budget, the oldest one is deleted. See Limits for every limit in ChartDeck in one place.

#Bars: run on fewer when you can

Run time grows with the number of bars. If your script only matters near the present (a session level, a dashboard, the last few swings), tell it so with calc_bars_count in indicator. The script then runs on that many of the newest bars instead of 5,000. A larger number (up to your plan's bars per run) is for scripts that need a long history; it costs run time in proportion:

ChartScriptOpen in editor
//@chartscript 1
indicator("Recent swings", { overlay: true, calc_bars_count: 1500 })
const len = input.int(10, "Swing length", { minval: 2, maxval: 100 })
const ph = ta.pivothigh(high, len, len)
const pl = ta.pivotlow(low, len, len)
plotshape(!na(ph), "Swing high", shape.triangledown, location.abovebar, color.down, { offset: -len, size: size.tiny })
plotshape(!na(pl), "Swing low", shape.triangleup, location.belowbar, color.up, { offset: -len, size: size.tiny })

#Loops

A loop inside a script runs on every bar. A loop of 500 passes on 20,000 bars is ten million passes in one full run, which is why the loop limits count passes per bar.

Use a built-in instead of a loop whenever one exists. The ta.* functions keep running state from bar to bar, so they do a little work per bar where a loop redoes everything. These two plots draw the same line; the second one is far cheaper:

ChartScriptOpen in editor
//@chartscript 1
indicator("Highest high, two ways", { overlay: true })
const len = input.int(200, "Length", { minval: 1, maxval: 1000 })

// slow: looks back over len bars on every bar
let hiLoop: float = na
for (let i = 0; i < len; i++) {
    hiLoop = na(hiLoop) ? high[i] : math.max(hiLoop, high[i])
}
plot(hiLoop, "Highest (loop)", color.muted)

// fast: the built-in keeps a running window
plot(ta.highest(high, len), "Highest (built-in)", color.orange, 2)

Other habits that help:

  • Exit early with break once you have found what you were looking for.
  • Do last-bar-only work on the last bar. Scanning back for levels, filling a table or summarizing the chart only needs doing once, under if (barstate.islast). Loops there run once, not on every bar.
  • Keep state instead of recomputing it. If you need the last ten swing highs, push each one into a state array as it is found, rather than searching for all ten on every bar.
  • Avoid nested loops over history. A loop inside a loop multiplies; a 200 × 200 scan is 40,000 passes per bar.

If a loop does go past the limit, the script stops with CS5301 (one loop) or CS5303 (all loops on one bar together).

#Drawings

Labels, lines, boxes, polylines and tables are objects that live from bar to bar. Creating one is more expensive than plotting a value, and the chart has to draw every one that exists.

  • Create once, then update. Keep a drawing in a state variable, create it the first time, and move it with its set_* functions afterwards. One object updated 20,000 times costs far less than 20,000 objects.
  • Draw only what you need. Use if (barstate.islast) { … } for anything that only describes the present.
  • Set the budget you really need. indicator("…", { max_labels_count: 20 }) keeps the chart light and the oldest labels go first.
  • Prefer plots for per-bar values. A plot or plotshape is drawn for you; a label per bar is not the right tool for a marker on every bar.
ChartScriptOpen in editor
//@chartscript 1
indicator("Last-bar label", { overlay: true, max_labels_count: 1 })
const len = input.int(50, "Length", { minval: 1 })
const avg = ta.sma(close, len)
plot(avg, "SMA", color.accent)

// one label, created once and moved on the last bar
state tag: label = na
if (barstate.islast) {
    if (na(tag)) {
        tag = label.new(
            bar_index,
            avg,
            "",
            { style: label.style_label_left, color: color.accent, textcolor: color.white }
        )
    }
    label.set_xy(tag, bar_index, avg)
    label.set_text(tag, "SMA " + str.tostring(avg, "#.##"))
}

Tables work the same way: create them with state, and fill their cells under if (barstate.islast). See Drawings and Tables.

#Requests to other timeframes and markets

Each different (market, timeframe) pair your script asks for through request.tf, request.market or request.lower_tf is one data context. Every context means more bars to load and another run of the requested expression on those bars. Your plan sets how many contexts one script may use (20 / 40 / 80 / 128); a script that asks for more gets CS3004 at compile time, or na for the extra ones at run time.

Ways to keep requests cheap:

  • Ask for several values in one request. A tuple costs one context, not one per value.
  • Reuse the same pair. Two requests to the same market and timeframe share one context.
  • Ask for higher timeframes when you can. A lower timeframe brings many bars per chart bar; ChartDeck loads only the part of it around what you are looking at, but it is still the most expensive kind.
ChartScriptOpen in editor
//@chartscript 1
indicator("Daily range and 4h trend", { overlay: true })
// one request, two values: one data context
const [dHigh, dLow] = request.tf("1D", [high, low])
const h4Ema = request.tf("240", ta.ema(close, 50))
plot(dHigh, "Daily high", color.up, { style: plot.style_stepline })
plot(dLow, "Daily low", color.down, { style: plot.style_stepline })
plot(h4Ema, "4h EMA 50", color.orange)

Your plan also sets how many markets and live streams update at once, and a chart's own price feed always comes first. Other-market requests past that keep their history and refresh every minute; the legend then says "Some data refreshes every minute on your plan", and request.live tells your script which ones. See Other timeframes and markets.

#max_bars_back

Reading history with a fixed offset (close[10]) costs nothing extra: ChartScript keeps exactly as many bars as the largest fixed offset needs. A dynamic offset (close[n] where n changes from bar to bar) cannot be measured in advance, so ChartScript keeps max_bars_back bars of that series: 500 by default, up to 10,000.

Only raise it when you need to, and as little as you can: every series that uses a dynamic offset keeps that many values. If one series needs a long history, set it for that series alone with max_bars_back(series, n) instead of for the whole script. When a script reaches further back than it keeps, it stops with CS5202, and the message suggests the value to use.

ChartScriptOpen in editor
//@chartscript 1
indicator("Change since the session start")
// how many bars back the session started: changes from bar to bar
state startBar: int = 0
if (session.isfirstbar) {
    startBar = bar_index
}
const back = bar_index - startBar
const price = close
max_bars_back(price, 2000)
plot(price - price[back], "Change since session start", color.accent)

#When a script is too slow

ChartScript measures every run and acts when a script takes too long. What you see:

What happenedCodeWhat ChartScript does
The full run took longer than 2,500 ms (5,000 ms on a phone)CS5401Stops the script; the message gives the time it reached.
One live update took longer than 50 msCS5402Counts it as slow.
Two slow live updates in a rowSwitches the script to bar close only: the legend says "Updating on bar close only (too slow per tick)". The script still updates at every bar close, just not between closes.
One slow live update aloneNothing visible: a single slow update (the tab waking up, for example) only re-runs the script from its bars.
The script stopped answeringCS5404Shuts it down. Other scripts on the same chart are reloaded and keep running.
The script used too much memoryCS5403Stops the script.

Bar-close-only mode is a safety net, not an error: the script's values at each close are the same as if it had updated on every price. But if your script is meant to react within a bar, make it faster. Profiling shows how to find the slow part.

#Protected scripts

A published script with protected source runs on ChartDeck's servers for everyone except its author (see Visibility and protection). The same idea applies there, with limits that follow the plan of the person viewing the chart:

Viewer's planBeginnerPlusProElite
Live updates per second, at most1234
A live update slower than this switches to bar close only15 ms25 ms40 ms60 ms
Protected charts open at once481220

Every bar close is always shown, on every plan. A chart nobody is looking at (a background tab, a hidden pane) gets no live updates until it is visible again, but still receives every bar close. When many viewers share the same script, market, timeframe and inputs, one run serves them all.

As the author, your own script runs in your browser like any private script, so test it there with a mind to these numbers: a protected script that needs 30 ms per update will be bar-close-only for Beginner and Plus viewers.

#See also

The ChartScript documentation is in English for now; translations will follow.