Great!
If you want to save chat history, you first need an online storage location since Hugging Face Spaces’ disk is ephemeral (it disappears after a restart). Using paid Persistent Storage is the simplest approach code-wise, but other methods exist, such as setting your dataset repository to private and storing it there.
There isn’t a single “official” Gradio/HF standard, but there is a very common pattern in systems like yours:
On the backend, log each turn (each user message + bot reply) as a small record, and periodically sync those logs to durable storage (disk and/or a dataset repo).
I’ll walk through:
-
Where history lives today (browser vs backend).
-
What “write as you go” vs “write at the end” really mean.
-
How this changes with:
- persistent storage enabled for the Space, and
- no persistent storage + upload to a private dataset repo.
-
A recommended pattern for your exact setup.
1. Where your chat history lives right now
1.1 In the browser (front-end side)
Gradio’s ChatInterface has a save_history flag that saves conversations in the user’s browser localStorage, and shows old conversations in a side panel in the Gradio UI. (Gradio)
Important:
- This is purely client-side.
- It’s only for the built-in Gradio UI.
- Your custom widget (HTML + JS using
@gradio/client) does not automatically use that browser history or write to it.
So that feature is nice UX for the Gradio demo page, but it’s not what you want for logging / datasets.
1.2 In memory on the backend (Python side)
Inside your chat(message, history) function:
history is the current full conversation for that session (in “messages” format).
- Gradio keeps this in memory while the session is alive, but does not persist it anywhere automatically. (Gradio)
On restart or scale-down, that in-memory history disappears unless you explicitly write somewhere. That’s why Gradio devs recommend “save the history to a JSON file or database yourself if you need persistence”. (GitHub)
So any durable logging is something you add yourself.
2. Conceptually: write-as-you-go vs write-at-the-end
Think of a conversation as a sequence of turns:
(user_1, bot_1), (user_2, bot_2), ...
“Write as you go” (incremental logging)
This is the standard approach in most production chat systems, because:
- Users rarely have a clean “end of chat” moment (they just close the tab).
- If the Space restarts, you’ve already saved everything up to the last successfully processed message.
- It matches normal logging/observability patterns: one event per request. (Hugging Face Forums)
“Write at the end” (batch / final snapshot)
This is less common for open-ended chat because:
- There is usually no robust “end” signal unless you add a “End chat” button.
- If the server dies or the user leaves before that event, you lose the whole conversation. (Hugging Face Forums)
Where people do use “final snapshot”:
- Short, well-bounded flows (e.g. multi-step forms, wizards).
- As an extra summary record, on top of incremental logging.
So: for your use case (general chatbot with widget), write-as-you-go is the usual and safer pattern.
3. How storage options change your choices
Now, overlay this on Hugging Face Spaces + private dataset repos.
3.1 With persistent storage enabled (/data)
When you enable persistent storage for a Space, you get a disk mounted at /data that survives restarts. You can read/write it like a normal filesystem. (Hugging Face)
At the same time:
- The Space’s git repo has its own strict limit (~1 GB for Spaces). It’s intended for code, not logs. You hit “Repository storage limit reached (Max: 1 GB)” if you try to keep growing data there. (Hugging Face Forums)
So the sane layout becomes:
- Code and small config → Space repo (max 1 GB, versioned).
- Logs and working data →
/data (persistent disk, not part of git).
- Long-term dataset / analytics → private dataset repo on the Hub.
Under this setup, the common pattern is:
Pattern A: incremental logs to /data, batched uploads to dataset repo
-
On every chat(message, history):
- Append one JSON line to
/data/chat_logs_YYYY-MM-DD.jsonl.
-
Periodically (time-based, or manually):
- Use
huggingface_hub’s upload_file() or upload_folder() to push the log file(s) into a private dataset repo (e.g. your-username/chatbot-logs). (Hugging Face)
Why this works well:
- Writing to
/data is fast and local; you can log every turn without worrying about network or Hub availability. (Hugging Face)
- Uploading logs in batches avoids spamming the dataset repo with many tiny commits, which can hit file/commit limits or be slow. HF recommend grouping changes instead of too many files per commit for large datasets. (Hugging Face)
- The dataset repo becomes your canonical, versioned store, separate from the Space’s 1 GB repo limit. (Hugging Face Forums)
“Write at the end” in this setup would mean:
- Only write to
/data when you detect an end-of-chat condition, then sync to the dataset repo.
- Still subject to the same problems if the process dies before “end”.
In practice, with persistent storage, “write as you go to /data, batch upload to dataset” is the sweet spot.
3.2 Without persistent storage
Without persistent storage:
- You still have up to ~50 GB of ephemeral disk available to the Space process, but it’s not guaranteed to survive restarts / rebuilds. (Hugging Face)
- If you write logs to the normal filesystem and the Space is rebuilt, those files disappear.
So options change:
Pattern B: incremental logs directly to remote (dataset / DB)
You can do:
For external databases/log services, this is standard and fine.
For a Hub dataset repo, you normally don’t want to push per message, because:
- Each
upload_file() is a git commit / revision on the Hub side. (Hugging Face)
- Too many small commits can be slow and, at large scale, hit operational limits.
So if you must log to a Hub dataset without persistent storage, you usually:
- Buffer logs in memory or a temporary file.
- Every N messages (or every M seconds), call
upload_file() to update a “current logs” file.
But now you’re back in a trade-off:
- More frequent uploads → fewer lost messages when the process dies, but more commits.
- Less frequent uploads → fewer commits, but you can lose more recent messages if the Space restarts before the next upload.
Without persistent storage, there’s no perfect solution using only the Hub; you either:
- Accept some risk of data loss (batch uploads), or
- Add an external logging backend that’s built for many small writes.
In other words:
- With persistent storage: log every turn to
/data, batch push to private dataset.
- Without persistent storage: best practice is really “use an external DB/logging service”; if you insist on only HF Hub, then do small batches to a dataset repo and accept that a crash before the next batch loses a few recent turns.
4. What people “usually” do in setups like yours
Putting this together, for your specific situation (Gradio Space + custom widget, and you’re open to persistent storage + private dataset):
-
Incremental logging is the norm.
- Record each
(user_message, bot_reply, history_snapshot) as one log event.
- This is the standard “event log” style in web backends and chat systems.
-
Avoid writing logs into the Space git repo.
- Space repos are deliberately capped at ~1 GB and intended to hold code and light assets. (Hugging Face Forums)
-
If you can enable persistent storage (/data):
- Append to
/data/chat_logs_YYYY-MM-DD.jsonl on every turn. (Hugging Face)
- Periodically upload those files to a private dataset repo (via
huggingface_hub.upload_file, with repo_type="dataset"). (Hugging Face)
-
If you cannot use persistent storage and insist on HF-only storage:
- Either accept some data-loss risk and batch log uploads to the dataset (e.g. once every N messages), or
- Add a real external logging backend (recommended if the logs are business-critical).
-
“Write only at end of chat” is unusual for open-ended bots.
- It’s mostly used for short, bounded workflows.
- For a general chatbot, you rarely know when the user is truly done, and a browser close or restart would lose everything if you wait.
5. A concrete “good default” for you
Given what you’ve described (Space + widget + interest in a private HF dataset), the pragmatic choice is:
-
Enable persistent storage for the Space (so /data is durable). (Hugging Face)
-
In chat(message, history):
- Generate your reply.
- Append a JSON record for that turn to
/data/chat_logs_YYYY-MM-DD.jsonl.
-
Once per day (or per N messages), run a small helper that:
- Pushes that day’s log file(s) to a private dataset repo using
huggingface_hub.upload_file() with repo_type="dataset". (Hugging Face)
That gives you:
- Robust per-turn logging.
- Safety against Space restarts (thanks to
/data).
- A clean, private dataset on the Hub you can later load with
datasets or just download as JSONL. (Hugging Face)
Here’s a small, self-contained example that does all three things:
- answers chats (
chat())
- logs each turn to
/data/chat_logs.jsonl (log_turn())
- can upload the log file to a private HF dataset repo (
upload_logs_to_hub())
I’ll show the code first, then walk through the important bits.
1. Complete app.py example
Assumptions (you can adjust names later):
- You’re running this in a Hugging Face Space.
- Persistent storage is enabled, so
/data behaves like a normal disk and survives restarts. (Hugging Face)
- You already created a private dataset repo like
your-username/chatbot-logs. (Zenn)
- Your Space has an HF write token set as a secret (e.g.
HF_TOKEN), so huggingface_hub can authenticate. (Medium)
# app.py
#
# End-to-end example:
# - chat(): handles one chat turn (used by Gradio + your widget)
# - log_turn(): appends each turn to /data/chat_logs.jsonl (JSONL format)
# - upload_logs_to_hub(): pushes that log file to a private HF dataset repo
import os
import json
from datetime import datetime
import gradio as gr
from huggingface_hub import HfApi # pip install huggingface_hub
# ------------- CONFIG -------------
# Local log file on the Space's persistent disk (/data)
LOG_PATH = "/data/chat_logs.jsonl"
# Your private dataset repo on the Hub (change this!)
HF_DATASET_REPO = "your-username/chatbot-logs" # repo_type="dataset"
# Make sure the /data directory exists
os.makedirs(os.path.dirname(LOG_PATH), exist_ok=True)
# One HfApi instance we can reuse
hf_api = HfApi()
# ------------- LOGGING -------------
def log_turn(message, history, response_lines):
"""
Append one chat 'turn' as a JSON object to LOG_PATH.
- message: current user message (dict when multimodal=True)
- history: full Gradio history in 'messages' format
- response_lines: list[str] returned to the user
"""
# Extract plain text from the current message
if isinstance(message, dict):
# multimodal=True → {"text": ..., "files": [...]}
user_text = message.get("text", "")
else:
user_text = str(message)
record = {
"timestamp": datetime.utcnow().isoformat(),
"user_text": user_text,
"response": response_lines,
"history": history, # list[{"role": ..., "content": ...}]
}
try:
with open(LOG_PATH, "a", encoding="utf-8") as f:
f.write(json.dumps(record, ensure_ascii=False) + "\n")
except Exception as e:
# Don't break the chat if logging fails
print(f"[log_turn] Failed to write chat log: {e}")
def upload_logs_to_hub():
"""
Upload the local log file to your private dataset repo.
This overwrites 'logs/chat_logs.jsonl' in the repo each time.
You can call this manually (e.g. via a button or a separate script).
"""
if not os.path.exists(LOG_PATH):
print("[upload_logs_to_hub] No log file yet; nothing to upload.")
return
try:
hf_api.upload_file(
path_or_fileobj=LOG_PATH,
path_in_repo="logs/chat_logs.jsonl",
repo_id=HF_DATASET_REPO,
repo_type="dataset", # VERY IMPORTANT: dataset repo, not model/space
)
print("[upload_logs_to_hub] Uploaded logs to dataset repo.")
except Exception as e:
print(f"[upload_logs_to_hub] Upload failed: {e}")
# ------------- CHAT HANDLER -------------
def chat(message, history):
"""
Core chat function used by Gradio and your JS widget.
- message: {"text": "<user text>", "files": [...]} (because multimodal=True)
- history: full chat history (messages format)
"""
# Basic echo-style logic (replace with your real AI logic)
if isinstance(message, dict):
user_text = message.get("text", "")
else:
user_text = str(message)
response_lines = [
"Hello from your Gradio Space!",
f"You said: {user_text}",
]
# Log this turn before returning
log_turn(message, history, response_lines)
return response_lines
# ------------- GRADIO APP -------------
demo = gr.ChatInterface(
fn=chat,
type="messages", # history is in OpenAI-style "messages" format
multimodal=True, # message is {"text": ..., "files": [...]}
title="Widget Demo Bot",
api_name="chat", # endpoint /chat (used by your JS widget)
)
if __name__ == "__main__":
demo.launch()
That’s the minimal “end-to-end” skeleton:
chat() is the same shape you already use for the widget.
log_turn() writes each turn to /data/chat_logs.jsonl.
upload_logs_to_hub() uses HfApi.upload_file() to push that file into your private dataset repo. (Hugging Face)
2. How and when to call upload_logs_to_hub()
The example just defines upload_logs_to_hub(); you choose when to call it. Common options:
The important thing is: logging each turn is local and cheap, and uploading to the Hub can be done less frequently in bigger chunks so you don’t spam the repo with tiny commits. (Hugging Face)
3. Why this pattern works well for you
Background / reasoning, in plain language:
-
Incremental logging (write as you go)
- Done inside
chat() via log_turn().
- You never lose an entire conversation just because the user closed the tab or the Space restarted; every message that got a response was written to disk.
- This matches how most backend systems and observability tools think: one log event per request.
-
Persistent storage at /data
- Hugging Face’s persistent storage mounts a disk at
/data that survives Space restarts. You read/write it like a normal folder. (Hugging Face)
- That’s why we use
LOG_PATH = "/data/chat_logs.jsonl" instead of writing into the repo itself (which is capped and versioned). You avoid Space repo storage limits and keep logs separate. (Hugging Face)
-
Private dataset repo for long-term storage / analysis
- Dataset repos are designed for larger data, can be private, and are easy to read later with
datasets.load_dataset or simple HTTP. (Hugging Face)
HfApi.upload_file() is the recommended one-file-at-a-time API: you pass the local path, the path_in_repo, and the repo_id with repo_type="dataset". (Hugging Face)
-
Keeps your Space repo light
- You’re not committing logs into the Space’s git repo, which has a ~1 GB limit and is meant for code and small assets, not growing logs. (Hugging Face)
From here, you can slowly add:
- A
conversation_id param, if you want to group log lines into conversations.
- More fields in
record (e.g. model name, latency, error flags).
- A small script that reads the JSONL from the dataset repo and turns it into a training/analysis dataset.