# How to Run a Gradio App in a Cloud Sandbox and Call Its API

> Install Gradio in a venv, launch with server_name="0.0.0.0", run it as a systemd service and host port 7860 on a private URL. To call it from gradio_client, trade the URL's _token for the proxy's cookie and pass it in headers. Our calls took about 0.6 seconds each.

Source: https://sandboxer.com/guides/run-a-gradio-app-in-a-sandbox. Updated 2026-10-27.

**Short answer:** Install Gradio in a venv, launch with server_name="0.0.0.0", run it as a systemd service and host port 7860 on a private URL. To call it from gradio_client, trade the URL's _token for the proxy's cookie and pass it in headers. Our calls took about 0.6 seconds each.

## What we ran

Everything below ran on production on October 27, 2026, on a `small` sandbox (2 vCPU, 4 GB), with Gradio 6.30.0 on the sandbox's Python 3.12 and `gradio_client` 2.7.2 on a laptop. Timings are single runs.

| Step | Result |
| - | - |
| `pip install gradio` into a fresh venv | 33 s, 347 MB |
| `systemctl restart gradio` until port 7860 answers | 2.4 to 2.9 s |
| Gradio process memory (RSS) | 139 MiB |
| Request without the URL token | `401` from the proxy |
| `client.predict(...)` from the laptop, with a file output | 588 to 612 ms per call |
| REST call with `curl` (POST, then read the result stream) | 0.4 s |
| `sbx stop --wait`, then `sbx resume` | 2.2 s stop, 1.3 s resume, `memoryRestored: true` |
| Reboot until the hosted URL answers again | 5.7 s |

A `small` sandbox is plenty for a CPU-only Gradio demo like this one. If your app loads a model, size for the model, not for Gradio.

## 1. Install Gradio and write the app

Gradio's [quickstart](https://www.gradio.app/guides/quickstart) installs it with pip. Python 3.12 is preinstalled, so make a venv and install into it. The install takes longer than `sbx exec`'s 30-second default, so raise the timeout:

```bash
sbx new --name gradio --type small --no-env
id=sb_r50cwqkv4c    # the id that sbx new printed

sbx exec $id --timeout 600 "mkdir -p ~/app && cd ~/app && python3 -m venv .venv && .venv/bin/pip install -q gradio"
```

The test app counts words and returns two outputs: a JSON object and a CSV file. The file output matters below, because files are where proxies usually break.

```python
# app.py
import collections, csv, re, tempfile
import gradio as gr

def word_counts(text: str, top_n: float):
    words = re.findall(r"[a-z']+", text.lower())
    top = collections.Counter(words).most_common(int(top_n))
    path = tempfile.NamedTemporaryFile(suffix=".csv", delete=False).name
    with open(path, "w", newline="") as f:
        csv.writer(f).writerows([("word", "count"), *top])
    return dict(top), path

demo = gr.Interface(
    word_counts,
    [gr.Textbox(label="Text", lines=6), gr.Slider(1, 20, value=5, step=1, label="Top N")],
    [gr.JSON(label="Counts"), gr.File(label="CSV")],
)
demo.launch(server_name="0.0.0.0", server_port=7860)
```

```bash
sbx scp app.py $id:app/app.py
```

`server_name="0.0.0.0"` is the line people miss. Gradio listens on `127.0.0.1` by default, and the hosting proxy then answers `502` ("Nothing is answering on port 7860"). Either bind to `0.0.0.0` as above (or set `GRADIO_SERVER_NAME=0.0.0.0`), or host the port with `--localhost`. Don't pass `share=True`: you don't need Gradio's share link when the sandbox already has an HTTPS URL.

## 2. Run it as a systemd service

A process started from `sbx exec` is gone after a reboot or a resume that boots from disk. A systemd unit comes back on its own:

```bash
sbx exec $id "sudo tee /etc/systemd/system/gradio.service >/dev/null <<'EOF'
[Unit]
Description=Gradio app
After=network-online.target

[Service]
User=user
WorkingDirectory=/home/user/app
ExecStart=/home/user/app/.venv/bin/python app.py
Environment=GRADIO_ANALYTICS_ENABLED=False
Restart=always

[Install]
WantedBy=multi-user.target
EOF
sudo systemctl enable --now gradio"
```

Plain `sbx exec` doesn't forward your local stdin; `sbx exec -i` does. We copied the file with `sbx scp` and wrote the unit with a heredoc inside the remote command. After `systemctl restart gradio`, port 7860 answered in 2.4 to 2.9 seconds; most of that is importing Gradio.

## 3. Host port 7860 on a private URL

```bash
sbx host $id 7860 --title "Word counts"
```

```text
https://global-vital-parrot-7860.box.sandboxer.com?_token=<token>
```

Open that link in a browser and the Gradio UI loads. The proxy sets a 30-day `__sbx_auth` cookie and redirects to `/`, and the cookie carries the page's later requests, including the result stream. Without the token, nothing reaches Gradio, not even its API:

```bash
B=https://global-vital-parrot-7860.box.sandboxer.com
curl -s -o /dev/null -w "%{http_code}\n" $B/
curl -s -o /dev/null -w "%{http_code}\n" $B/gradio_api/info
curl -si -c jar "$B?_token=<token>" | grep -iE '^HTTP|^location'
```

```text
401
401
HTTP/2 302
location: /
```

The token, the cookie and switching between public and private are covered in [Give a sandbox port an HTTPS URL](/guides/give-a-sandbox-port-an-https-url). This guide covers what's specific to Gradio: calling the app from code.

## 4. Call the app from gradio_client

[`gradio_client`](https://www.gradio.app/guides/getting-started-with-the-python-client) is Gradio's Python client (`pip install gradio_client`). Give it the private URL as is and it fails in both forms:

```text
base URL, no cookie -> AuthenticationError Could not load https://global-vital-parrot-7860.box.sandboxer.com/ as credentials were not provided. Please login.
URL with ?_token=   -> AuthenticationError Could not load https://global-vital-parrot-7860.box.sandboxer.com?_token=<token> as credentials were not provided. Please login.
```

The client builds its own paths from the base URL and drops the query string, so the token never reaches the proxy. Trade the token for the cookie first and pass it with `headers=`:

```python
# client.py: reads the private hosted URL, with its ?_token=, from ./url
import time, httpx
from gradio_client import Client

private_url = open("url").read().strip()

# Trade the ?_token= for the proxy's __sbx_auth cookie.
r = httpx.get(private_url, follow_redirects=False)
cookie = r.headers["set-cookie"].split(";")[0]
base = private_url.split("?")[0]

client = Client(base, headers={"Cookie": cookie}, verbose=False,
                analytics_enabled=False, download_files="downloads")
print(client.view_api(print_info=False, return_format="dict")["named_endpoints"].keys())

text = "the cat sat on the mat and the dog sat too"
for i in range(5):
    t = time.perf_counter()
    counts, csv_path = client.predict(text, 3, api_name="/word_counts")
    print(f"{(time.perf_counter() - t) * 1000:.0f} ms", counts)
print(open(csv_path).read())
```

```text
dict_keys(['/word_counts'])
612 ms {'the': 3, 'sat': 2, 'cat': 1}
588 ms {'the': 3, 'sat': 2, 'cat': 1}
595 ms {'the': 3, 'sat': 2, 'cat': 1}
593 ms {'the': 3, 'sat': 2, 'cat': 1}
593 ms {'the': 3, 'sat': 2, 'cat': 1}
word,count
the,3
sat,2
cat,1
```

Three things to notice:

- **The endpoint name is the function name.** Gradio named it `/word_counts` without any `api_name` in the code. `view_api()` lists the names if you aren't sure.
- **File outputs come back through the same URL.** `predict` downloaded the CSV through the proxy with the same cookie. Each call includes that download, which is part of the roughly 0.6 seconds.
- **The cookie is a credential.** It works for 30 days, like the token. Keep both out of logs and source code.

Don't swap in Gradio's own `auth=` login for this. It adds a password to the app, but the proxy's token is what keeps requests from reaching it at all. If you make the port `--public` instead, anyone with the URL can call your API, and `Client(base)` then works with no headers.

## 5. Call it with curl

Gradio also has a plain [HTTP API](https://www.gradio.app/guides/querying-gradio-apps-with-curl): POST the inputs, get an `event_id`, then read the result as a server-sent event stream. Through a private URL, send the cookie jar from step 3 with both requests:

```bash
EID=$(curl -s -b jar -H 'Content-Type: application/json' -X POST $B/gradio_api/call/word_counts \
  -d '{"data": ["the cat sat on the mat and the dog sat too", 3]}' \
  | python3 -c 'import json, sys; print(json.load(sys.stdin)["event_id"])')
curl -s -b jar -N $B/gradio_api/call/word_counts/$EID
```

```text
event: complete
data: [{"the": 3, "sat": 2, "cat": 1}, {"path": "/tmp/gradio/4427…/tmpp424nhik.csv", "url": "https://global-vital-parrot-7860.box.sandboxer.com/gradio_api/file=/tmp/gradio/4427…/tmpp424nhik.csv", "size": 33, "orig_name": "tmpp424nhik.csv", "mime_type": null, "is_stream": false, "meta": {"_type": "gradio.FileData"}}]
```

Both requests together took 0.4 seconds. The file's `url` already uses the public `box.sandboxer.com` hostname, so we didn't need to set `root_path` or any proxy option in Gradio. Fetch it with the same cookie jar. The same POST without the cookie got `401`.

This form suits agents and shell scripts that can't install `gradio_client`.

## 6. What survives a stop and resume

```bash
sbx stop $id --wait
sbx resume $id
sbx exec $id "systemctl show gradio -p MainPID -p ActiveEnterTimestamp"
```

```text
MainPID=1920
ActiveEnterTimestamp=Tue 2026-10-27 10:03:10 UTC
```

The stop took 2.2 seconds and the resume 1.3 seconds, with `memoryRestored: true`. The service's start time was still from before the stop and the VM had not rebooted, so Gradio never restarted. `client.py`, run again with the same private URL, returned the same counts. Routes and tokens survive a stop, so links you already shared keep working.

A resume boots from disk instead when no machine with the same CPU type is free (see [snapshots](/docs/snapshots#resume)). To check that path, we rebooted the sandbox: the enabled unit started Gradio, and the hosted URL answered `200` 5.7 seconds after the reboot command. The venv and `app.py` are on the sandbox disk, so nothing needs reinstalling.

An idle sandbox pauses itself after `idleStopMinutes` (30 on our test sandbox). Requests to the hosted URL count as activity. See [Idle auto-pause and wake on use](/guides/idle-auto-pause-and-wake-on-use).

## 7. Clean up

```bash
sbx delete $id --yes
```

A `small` sandbox costs $0.018/hour while it runs and nothing while it is stopped.

## FAQ

### Why does my Gradio URL show "Nothing is answering on port 7860"?

Gradio binds to `127.0.0.1` unless you tell it otherwise. Launch with `server_name="0.0.0.0"` or `GRADIO_SERVER_NAME=0.0.0.0`, or host the port with `sbx host $id 7860 --localhost`.

### Why does gradio_client say "credentials were not provided"?

The URL is private and the client sent neither the token nor the cookie. It drops the `?_token=` query, so fetch the URL once without following redirects, take the `__sbx_auth` cookie from `Set-Cookie`, and pass it as `Client(base, headers={"Cookie": cookie})`.

### Do I need share=True?

No. `share=True` creates a separate share link through Gradio's servers. The sandbox's hosted URL already gives you HTTPS, and it is private by default.

### Do file inputs and outputs work through the hosted URL?

File outputs did in our test: Gradio returned a `box.sandboxer.com` file URL, and `gradio_client` downloaded the CSV through the proxy. We didn't test file uploads.

## Related

- [Give a sandbox port an HTTPS URL](/guides/give-a-sandbox-port-an-https-url)
- [Build a stateful code interpreter with a Jupyter kernel](/guides/build-a-stateful-code-interpreter-with-jupyter)
- [Run Ollama on CPU in a cloud sandbox](/guides/run-ollama-on-cpu-in-a-cloud-sandbox)
- [Hosting](/docs/hosting)
