What Chainlit actually is
Chainlit is an open-source Python framework built specifically for creating chat interfaces for LLM applications. The technical reality is that it wraps around your existing Python code (LangChain, LlamaIndex, raw OpenAI calls, anything) and gives you a web-based chat UI without you writing HTML, CSS, or JavaScript.
Under the hood it runs a FastAPI server, serves a React-based frontend, and uses WebSockets for streaming responses. When you write a Chainlit app, you are decorating Python functions with handlers like @cl.on_chat_start and @cl.on_message. Chainlit intercepts those, manages session state, and pushes messages back to the browser.
It is not a chatbot engine. It is not an LLM. It is a presentation layer that sits on top of whatever logic you already have. If you already have a working Python script that calls an LLM, Chainlit is the thinnest possible wrapper to put a UI on it.
The framework was originally built by the team at Chainlit (the company) and is now maintained as an open-source project. The current version supports Python 3.9 and above, integrates with most major LLM providers, and ships with built-in support for streaming, file uploads, and authentication.
Setup and authentication
Installation is straightforward. Run:
pip install chainlit
Then create a single file, conventionally app.py:
`import chainlit as cl
@cl.on_chat_start async def start(): await cl.Message(content=“Hello, how can I help?“).send()
@cl.on_message async def main(message: cl.Message): response = f”You said: {message.content}” await cl.Message(content=response).send()`
Run it with:
chainlit run app.py -w
The -w flag enables watch mode so changes reload automatically when you edit the file.
For authentication, Chainlit supports two modes out of the box. The first is built-in password authentication, configured through a .chainlit/config.toml file:
[auth] require_login = true default_username = "user" default_password = "mypassword"
The second is OAuth through providers like GitHub, Google, or Azure AD. You configure this in the same config.toml under [auth.github] or [auth.google] sections, providing client_id and client_secret. Chainlit handles the callback flow and exposes the authenticated user object to your handlers via cl.user_session.
For production deployments you typically run Chainlit behind a reverse proxy (nginx, Caddy) with HTTPS termination, and pass through the auth headers. The framework is designed to integrate with existing identity infrastructure rather than replace it.
First working example
Here is a working example with OpenAI. This assumes you have an OpenAI API key set as an environment variable:
`import os import chainlit as cl from openai import AsyncOpenAI
client = AsyncOpenAI(api_key=os.environ.get(“OPENAI_API_KEY”))
@cl.on_chat_start async def start(): cl.user_session.set(“messages”, []) await cl.Message(content=“Ask me anything.”).send()
@cl.on_message async def main(message: cl.Message): messages = cl.user_session.get(“messages”) messages.append({“role”: “user”, “content”: message.content})
msg = cl.Message(content="")
await msg.send()
stream = await client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
stream=True
)
async for chunk in stream:
if chunk.choices[0].delta.content:
await msg.stream_token(chunk.choices[0].delta.content)
messages.append({"role": "assistant", "content": msg.content})
cl.user_session.set("messages", messages)`
The cl.Message.stream_token() method is what gives you the typewriter effect in the UI. Without it, the response appears all at once when the LLM finishes. This single line of code is the difference between a polished chat experience and a clunky one.
To add LangChain integration, install the extra:
pip install chainlit[langchain]
Then you can use the @cl.langchain_factory decorator or @cl.langchain_run decorator to wrap LangChain agents and chains. Chainlit will automatically render tool calls, intermediate steps, and retrieval steps in the UI. If you are building a RAG system, this is the fastest path to a visible interface.
To run the example, set your API key and start the server:
export OPENAI_API_KEY=your_key_here chainlit run app.py -w
Your browser will open to http://localhost:8000 and you will see a working chat interface.
Key settings that matter
The .chainlit/config.toml file controls most behavior. Here are the settings that actually matter in practice.
Chat settings control the UI defaults. chat_profiles lets you define multiple personas or system prompts that users can switch between. Each profile has its name, markdown description, and icon. This is useful when you want one app to serve different audiences (support, sales, technical).
Message settings include the welcome message, input placeholder, and default language. The welcome message supports Markdown and is shown above the chat input on first load. Set this to something specific to your use case rather than leaving the default.
UI customization lives under [UI] with options for default_theme (light or dark), custom_css, and custom_js. You can inject your own styles without forking the codebase. The custom CSS escape hatch works for small tweaks like changing the primary color or font.
Headless mode is controlled by the headless setting. When true, Chainlit runs without the browser opening automatically. This is essential for CI/CD and server deployments where there is no display.
Cache settings under [cache] control how long message and element caches persist. The default is reasonable for development but you will want to tune this for production. If you are storing sensitive data, set shorter cache windows.
The most overlooked setting is probably the one under [project]. The default project name is “Chatbot” and shows up in the browser tab. Change it to your actual product name. This is the kind of detail that separates a prototype from something that feels finished.
Session timeout determines how long user sessions persist. For internal tools this can be hours. For customer-facing apps you might want shorter windows. The default is 30 minutes.
Telemetry can be disabled with the telemetry setting. By default Chainlit collects anonymous usage data. If that is a concern for your organization, turn it off explicitly in the config file.
Where it shines
Chainlit works well in three specific scenarios.
Internal LLM tooling for teams. When you need to give non-technical colleagues access to a RAG system, an agent, or a fine-tuned model, Chainlit gets you a usable interface in an afternoon. The auth integration means you can gate access without building a login flow.
Prototyping agent systems. If you are building with LangChain or LlamaIndex and need to see what your agent is actually doing, Chainlit’s step visualization is genuinely useful. Tool calls, retrieval results, and intermediate reasoning show up in the chat thread without extra work. This makes debugging agent loops much faster.
Demo and customer-facing prototypes. When you need to show a client what a chatbot could look like without committing to a full frontend build, Chainlit is fast enough to spin up in a day. The UI is clean enough for screenshots and the streaming response feels professional.
Data exploration with LLMs. Pairing Chainlit with a Pandas DataFrame or a SQL connector gives analysts a conversational interface to data without building a custom frontend. The element rendering (cl.Dataframe, cl.Plotly) lets you return charts and tables inline in the chat thread.
Multi-user testing environments. Because Chainlit handles session state per user, you can deploy one instance and have multiple testers hitting it simultaneously. Each gets their own conversation history and context.
Where it fails
Honest list of where Chainlit struggles.
Production-grade frontend customization. The UI is opinionated. If your design team has specific requirements around spacing, typography, or interaction patterns, you will fight the framework. The custom CSS escape hatch works for small tweaks but not for full redesigns.
Mobile responsiveness. The default UI works on mobile but is not optimized for it. If your users are primarily on phones, expect to spend time on CSS adjustments and testing across devices.
Stateful conversations across sessions. Chainlit stores session state in memory by default. For long-running conversations or resumable chat history, you need to wire up your own persistence layer. The framework does not ship with a database backend.
Multi-modal inputs. File uploads work for images and documents, but the input UX for complex multi-modal interactions (drag-and-drop, voice, camera) is limited. You would need to extend the frontend significantly.
Cost observability. Chainlit does not track token usage or API costs per conversation out of the box. For production deployments where cost matters, you need to instrument this yourself by wrapping your LLM calls.
Scaling beyond single-server. Chainlit runs as a single FastAPI process. For high-traffic deployments you would want to run multiple instances behind a load balancer, but the session management assumes a single process. WebSocket connections need sticky sessions.
The dependency on decorators. If your application architecture does not fit the handler pattern (long-running background jobs, event-driven flows), Chainlit becomes awkward to use. It is optimized for the chat loop, not general async Python apps.
Practical workflow pattern
Here is how to slot Chainlit into a real work setup.
Development phase. Use Chainlit as your primary interface while building. It is faster than building a custom UI and gives you immediate feedback on what your LLM is producing. Keep your core logic in separate modules (a rag.py, an agent.py) and have app.py just import and wire them up to Chainlit handlers. This separation makes testing easier and keeps your business logic reusable.
Testing phase. Run Chainlit in headless mode during automated tests. Use the Chainlit SDK to programmatically send messages and assert on responses. The framework exposes a test client for this purpose. You can simulate user conversations and verify that your handlers produce expected output.
Internal rollout. Deploy Chainlit behind your SSO using the OAuth integration. Run it on a small VM or container with HTTPS through a reverse proxy. Set session timeouts based on your security requirements. For most internal tools, a single small instance handles dozens of concurrent users.
Customer-facing prototype. Use Chainlit for the first version, then evaluate whether the UI constraints are acceptable. If clients need specific branding or interaction patterns, plan to migrate to a custom frontend (Next.js, Streamlit, or Gradio depending on the use case). The migration cost is low if your business logic is decoupled from the UI layer.
Long-term. Most teams use Chainlit as a stepping stone. It gets you to a working product fast, then you either keep it as an internal tool or migrate the customer-facing surface to a custom build. The Python logic stays the same either way.
One practical tip: keep your Chainlit app thin. If you find yourself writing complex logic inside @cl.on_message handlers, that is a signal to extract it. The handlers should be glue code between your real application and the UI layer. When your app.py is more than 200 lines, something probably belongs in a separate module.
Another tip: use the data layer feature for persistence. Chainlit supports a data layer abstraction that lets you store conversation history in a database without changing your handler code. This is the right way to handle long-term memory and session resumption.
If this is the kind of problem agents can help with, the free Working With Claude field guide is the practical next step. Thirty-two pages, no fluff. Get the free guide.