Limits & performance
A GDN app runs inside a sandbox with hard guardrails, so one app can never hang the panel or overload a data source. Most apps never come close to any of these — but when a render stops unexpectedly, it's usually one of them. Here they all are in one place.
Canvas
| Limit | Value |
|---|---|
| Height | Always 32 px. |
| Width | 1–384 px. One panel module is 64 px wide; they daisy-chain (64, 128, 192, …). |
| Recommended width | Keep images to 192 px or smaller for best performance, and split content across pages instead of going very wide. |
Anything drawn outside the canvas is clipped — it's not an error, it just doesn't show.
Drawing
Each page may issue at most 4096 draw operations. Every c.* call is one op (a
helper like c.table expands into several). Go over and the render stops with:
error: op limit (4096) exceeded on a single page
If you hit it, you're almost certainly drawing per-pixel where a shape or helper would do — or you should split the screen across pages.
Time
Each render is killed if it runs longer than 30 seconds of wall-clock time:
error: render timed out (>30s) — possible infinite loop
That budget has to cover any http.get calls too (each with its own timeout, below), so
in practice your own code should finish in well under a second. This limit exists to
stop runaway loops — the usual trigger is a loop that never terminates.
On Glance's servers a render also runs under a CPU cap (~25 CPU-seconds) and a memory cap (~512 MB). Local previews on your own machine don't apply the CPU/memory caps, so an app that's fine locally can still hit them once published — keep renders light.
HTTP
http.get is the only network access, and it's bounded:
| Limit | Value | What happens at the edge |
|---|---|---|
| Requests per render | 8 uncached | A 9th raises http limit: at most 8 …. Cache hits don't count. |
| Per-request timeout | ~4 seconds | A slower endpoint returns status_code 0 (with resp["error"] set) instead of hanging. |
| Response size | 1 MB | Larger bodies are truncated. Panel apps need kilobytes — pick a smaller endpoint. |
| Method | GET only | Panel apps read data; they don't post it. |
| Default cache | 300 s (ttl_seconds) | 2xx responses are cached by (url, params, headers); repeat renders reuse them. |
Because a slow API surfaces as status_code 0, always draw a fallback when
resp["status_code"] != 200. Pick APIs that answer quickly — see
where to find them.
Refresh
refresh: in the manifest sets how often the panel re-renders, in seconds:
| Value | Use |
|---|---|
60 | The minimum — validation fails below it. Clocks only. |
300 | A good default (5 minutes). |
900 | 15 minutes. |
3600 | Hourly data (weather, once-a-day counts). |
A fast refresh doesn't make a still frame animate — it just re-fetches and redraws more often, which is harder on your data source and rarely needed. See How often your app redraws.
Refresh and inputs
How often an app refreshes decides which inputs and how many pages it may have:
refresh | Inputs | Setting combinations | Pages | Live data |
|---|---|---|---|---|
| Under 60 s | Not allowed | |||
| 60 to 119 s | Pick-lists only | At most 48 | At most 3 | None |
| 120 to 299 s | Pick-lists only | At most 100 | At most 3 | Yes |
| 300 to 899 s | Anything except color | No limit | No limit | Yes |
| 900 s or more | Anything | No limit | No limit | Yes |
A pick-list is a dropdown, checkbox, or selection input. free-text,
api-key, date, date-past, number, and color inputs are not. "Live data" means
http.get; under 2 minutes, only reference data cached with ttl_seconds of 3600 or
more is allowed.
Validation checks each rule:
| Rule | Validation message |
|---|---|
refresh is at least 60 | refresh 30s is below the 60s minimum |
| Under 5 minutes, every input is a pick-list | refresh 180s is under 5 minutes, so every setting must be a pick-list: station is free-text |
| Under 15 minutes, colors come from a dropdown, not a color picker | setting accent is a color picker on an app that refreshes every 60s; under 15 minutes use a dropdown of named colors |
| Under 5 minutes, settings stay within the combination limit | settings allow 462 combinations; an app that refreshes every 60s may allow at most 48 |
| Under 2 minutes, no live data | app fetches live data every 60s; apps that fetch data refresh every 2 minutes or slower |
| Under 5 minutes, at most 3 pages | 4 pages on an app that refreshes every 120s; under 5 minutes the limit is 3 |
http.get caches for at least 60 seconds (a warning) | http.get ttl_seconds=20 is under 60s; cache responses for at least 60 seconds |
Counting combinations
Multiply the options in every input:
| Input | Counts as |
|---|---|
dropdown | Its number of choices |
checkbox | 2 |
selection | 2 multiplied by itself once per choice |
A time-zone dropdown (4) and a color dropdown (8) is 4 x 8 = 32.
Colors on a fast app
Under 15 minutes, offer the Glance accent palette as a dropdown. Every name is a color
the drawing API accepts, so the value goes straight into color=:
- key: accent
app_input_type: dropdown
type: choice
label: Accent color
default: amber
choices: [white, red, orange, amber, green, skyblue, purple, pink]
If a published app used a color picker, keep the same key and map any value that isn't
on the list to your default, since panels keep the values people already saved:
PALETTE = ["white", "red", "orange", "amber", "green", "skyblue", "purple", "pink"]
def accent_color(ctx):
pick = str(ctx.inputs.get("accent", "amber")).lower()
return pick if pick in PALETTE else "amber" # an older saved value may be a hex color
Inputs
An app may make 8 uncached requests per render regardless of how many inputs it has, and there's no fixed cap on the number of inputs — but every declared input must be read by your code, or validation fails it.
See also
- Troubleshooting — what each limit's error looks like.
- HTTP requests — caching and failure handling in full.
- manifest.yaml — where
width,refresh, and the rest live.