Plotly and Bokeh both produce interactive, browser-based charts from Python — hover tooltips, zoom, pan, the works. The difference is altitude. Plotly Express gets you a polished interactive chart in one function call with smart defaults. Bokeh gives you granular control over every widget, callback, and DOM element, which pays off when you need interactivity beyond what a charting library normally offers.
How do the rendering architectures differ?
Both generate output that runs in the browser, but the mechanics diverge in an important way.
Plotly creates a JSON spec in Python and hands it to plotly.js for browser-side rendering. Your Python code describes the chart; the browser draws it.
Bokeh also generates JSON, but renders via BokehJS, its own TypeScript runtime. Bokeh's document model maintains a server-side state that mirrors the client — this two-way sync is what makes Bokeh Server possible.
Plotly Bokeh
┌──────────────┐ ┌──────────────┐
│ Python code │ │ Python code │
│ (Express / │ │ (figure / │
│ graph_objs) │ │ models) │
└──────┬───────┘ └──────┬───────┘
│ JSON spec │ JSON doc
▼ ▼
┌──────────────┐ ┌──────────────┐
│ plotly.js │ │ BokehJS │
│ (browser) │ │ (browser) │
└──────────────┘ └──────┬───────┘
│ websocket
▼
┌──────────────┐
│ Bokeh Server │
│ (Python proc)│
└──────────────┘That websocket is the key architectural difference. Plotly charts are fire-and-forget — Python creates the spec, the browser renders it, done. Bokeh charts can maintain a live connection back to a running Python process, so your callbacks execute in Python, not JavaScript.
How do the APIs feel in practice?
Plotly Express is the fastest path to an interactive chart I've used in Python:
import plotly.express as px
fig = px.scatter(df, x="ad_spend", y="conversions",
color="channel", size="roas",
hover_data=["campaign_name"])
fig.show()Five lines — interactive scatter plot with color legend, size encoding, hover tooltips. Bokeh's equivalent takes more setup:
from bokeh.plotting import figure, show
from bokeh.models import ColumnDataSource, HoverTool
source = ColumnDataSource(df)
p = figure(width=800, height=400, title="Ad Spend vs Conversions")
p.scatter("ad_spend", "conversions", source=source,
size="roas", color="color_mapped",
legend_field="channel")
p.add_tools(HoverTool(tooltips=[
("Campaign", "@campaign_name"),
("ROAS", "@roas{0.2f}")
]))
show(p)More code, but you get explicit tooltip formatting, direct column references, and the ColumnDataSource pattern that enables cross-filtering and linked selections later. Learning curve reflects the trade-off — Express: 20 minutes to productive. Bokeh: an afternoon, then it clicks.
Fastero
Connect your database. Ask questions. Get dashboards.
Postgres, BigQuery, Snowflake, and 10+ sources — live-connected, AI-powered, no dashboard builder learning curve.
Try free →What does the feature comparison look like?
| Dimension | Plotly | Bokeh |
|---|---|---|
| One-liner charts | Plotly Express — genuinely one call | No equivalent; always multi-step |
| Default aesthetics | Polished, modern out of the box | Functional, needs more styling work |
| Hover/zoom/pan | Built-in on every chart | Built-in, with more granular tool config |
| Custom widgets | Via Dash (separate framework) | Native — sliders, dropdowns, text inputs |
| Server-side callbacks | Dash (separate process) | Bokeh Server (built-in) |
| JS callbacks | Limited (no official API) | First-class CustomJS callbacks |
| Pandas integration | Pass DataFrames directly to Express | ColumnDataSource wraps DataFrames |
| 3D charts | Excellent (WebGL) | Limited, not a strength |
| Statistical charts | Box, violin, histogram, density in Express | Box, histogram; fewer presets |
| Streaming data | Via Dash intervals | Native ColumnDataSource.stream() |
| Chart types | 50+ trace types | Fewer presets, flexible glyph primitives |
| Embedding in web apps | HTML export, iframe, Dash | HTML export, embed.js, Bokeh Server |
| Large datasets | WebGL mode to ~1M points | WebGL + Datashader integration |
| Docs quality | Extensive, well-organized | Good, harder to navigate |
| Community size | Larger (Plotly + Dash ecosystem) | Smaller, HoloViz-adjacent |
When does Bokeh's interactivity model actually matter?
Plotly charts ship with excellent defaults — hover, zoom, pan, box select, lasso select. But if you want a slider that re-queries a database and updates a chart, you need Dash. At that point you're building a web application, not configuring a chart.
Bokeh blurs that line. Widgets go directly into a Bokeh layout, wired to Python callbacks via Bokeh Server, and the chart updates in-place over a websocket:
from bokeh.layouts import column
from bokeh.models import Slider
from bokeh.plotting import figure, curdoc
p = figure(width=600, height=300)
line = p.line(x, y)
slider = Slider(start=1, end=100, value=10, title="Window")
def update(attr, old, new):
# This runs server-side in Python — full access to pandas, numpy, your DB
smoothed = df["value"].rolling(slider.value).mean()
line.data_source.data["y"] = smoothed
slider.on_change("value", update)
curdoc().add_root(column(slider, p))That callback runs in Python on the server — it can query a database, call an API, run NumPy. The result patches the client via websocket. This is Bokeh's genuine advantage: server-side Python callbacks with live chart updates, no Dash required.
How do Dash and Bokeh Server stack up as app frameworks?
Dash Bokeh Server
┌─────────────────┐ ┌─────────────────┐
│ Flask + React │ │ Tornado server │
│ (full SPA) │ │ + websockets │
├─────────────────┤ ├─────────────────┤
│ @callback deco │ │ on_change() │
│ HTTP round-trip │ │ live doc sync │
└─────────────────┘ └─────────────────┘Dash is a full web application framework — React, rich components, multi-page apps, auth plugins. Declarative callbacks over HTTP. Bokeh Server is lighter — it syncs a Bokeh document between Python and browser over websockets, great for interactive visualizations with server-side logic, but less suited for full apps with navigation.
Dash wins for production apps. Bokeh Server wins when you just need Python callbacks on charts without a full framework. For the broader comparison, see our Streamlit vs Dash deep dive.
How do they handle large datasets?
Both hit the same browser-side bottleneck: too many data points in the DOM kills performance. Both offer WebGL rendering as the escape hatch.
Plotly's Scattergl and render_mode='webgl' push rendering to the GPU — roughly 500k-1M scatter points. Beyond that, you're downsampling (see Plotly vs Matplotlib). Bokeh's WebGL is comparable, but its edge is Datashader integration via HoloViz — server-side rendering of millions of points into an image, with hover still working on aggregated data.
For datasets under 100k points — most business dashboards — performance is a non-issue with either library.
Which library should you pick?
Pick Plotly when:
- You want interactive charts with minimal code (Express is unmatched)
- You're building a data app with Dash or Streamlit
- You need 3D, geo maps, or statistical charts out of the box
- You're embedding charts in existing web apps via HTML export
Pick Bokeh when:
- You need server-side Python callbacks without a full app framework
- You're building custom interactive tools (linked brushing, streaming, custom selections)
- You're in the HoloViz ecosystem (Panel, HoloViews, Datashader)
- You need
CustomJScallbacks for client-side interactivity without a server
The honest default: for most data engineers building dashboards from database data, Plotly is the better starting point. Faster to learn, better defaults, and Dash gives you an upgrade path. Bokeh shines in specific niches: streaming data, custom scientific tools, and apps where every interaction needs server-side Python.
Both still require writing Python, managing dependencies, and handling deployment. If what you actually need is interactive dashboards from your database, Fastero skips the code entirely — connect your data source, describe what you want, and get a live dashboard.
FAQ
Can I use Plotly and Bokeh in the same project? Yes, but there's rarely a reason to. They solve the same problem with different trade-offs. If you're using Panel as your app framework, it renders both natively — that's the one scenario where mixing makes sense.
Is Bokeh harder to learn than Plotly?
The ColumnDataSource/glyph model takes longer to internalize than px.scatter(), but once it clicks, Bokeh's explicitness makes complex charts easier to debug. Budget 2-3 hours for the fundamentals vs 30 minutes for Plotly Express.
Does Plotly work with Grafana or other dashboard tools? Not directly — Plotly charts are self-contained HTML/JS and don't plug into Grafana's panel system. We covered that trade-off in Grafana vs Python for Data Dashboards.
Can Bokeh Server handle production traffic? For internal tools with a few concurrent users, yes. Each user gets a dedicated Python session, so memory scales linearly with connections. For hundreds of concurrent users, you'll need reverse proxies and session limits.
Which has better pandas integration?
Plotly Express accepts DataFrames directly — columns become axis references. Bokeh wraps DataFrames in ColumnDataSource, adding a step but giving explicit control over live updates via .stream() and .patch(). Plotly is more convenient for one-off charts; Bokeh is more capable for live-updating ones.
What about Altair as a third option? Altair is declarative (grammar-of-graphics) and renders via Vega-Lite. Elegant for statistical visualization, but with a lower customization ceiling. Good for exploratory analysis; for dashboards, Plotly or Bokeh give you more control.
Try Fastero free — interactive dashboards from your database, no Python setup or chart library decisions. No credit card required.

