The cell succeeds, the plot disappears, and nobody gets paged
A Databricks community thread captured a pattern that R users on the platform know too well: plot(1:3, 5:7) executes without error on Runtime 14.3 LTS, the cell shows a green checkmark, and the output area is empty. No warning. No traceback. The same cluster renders Python matplotlib plots normally.
This is not a rare edge case. It surfaces repeatedly across Databricks Community forums, and the resolution is almost always the same vague arc — the problem appears after a platform update, persists for days, then quietly resolves. The original poster in this thread confirmed the issue disappeared without any change on their end. Databricks never disclosed what broke.
The professional stakes are real. R notebooks that generate ggplot2 visualizations for weekly reporting, QA charts embedded in data validation workflows, exploratory plots during feature engineering — all of these produce invisible output. The notebook looks like it ran. The scheduler marks the job as succeeded. A stakeholder opens the notebook expecting a chart and finds nothing.
What makes this failure mode dangerous is its invisibility. A job that throws an exception gets caught by alerting. A job that completes successfully but produces empty output passes through every checkpoint. Unless you've built explicit validation around rendered output, you won't know until someone manually opens the notebook and notices the gap.
Databricks wraps R graphics in a PNG device you don't control
When you execute a plot command in an R cell, Databricks doesn't render it the way RStudio does. RStudio sends graphics instructions to a local rendering backend — the RStudioGD device — that you can inspect and configure. Databricks instead intercepts R's graphics output through a server-side PNG device.
The mechanism works like this: before your R cell executes, Databricks opens a PNG graphics device. Your plotting code writes to that device. After execution, Databricks reads the resulting PNG bytes and embeds them as base64-encoded images in the notebook's output JSON. The R REPL itself never "sees" the rendered image — it just writes to a file descriptor that Databricks manages.
This is why dev.list() returns a device even when plots aren't rendering. The device exists. It accepts write calls. It just doesn't produce usable output. The failure happens in the layer between the device output and the notebook frontend — a layer entirely outside R's error-handling scope.
Several conditions can break this pipeline. A Databricks Runtime update can change how the notebook server initializes graphics devices. An interrupted cell can leave the device in a half-open state where subsequent plots write to a corrupted buffer. Library conflicts between R's built-in grDevices and packages like Cairo or ragg can redirect output to a device that Databricks doesn't read from.
The png() and dev.off() cycle that works in standalone R scripts doesn't help here because Databricks has already opened its own device before your code runs. Calling dev.off() on Databricks' device without opening a replacement leaves subsequent cells with no device at all — and still no error.
The DBFS workaround works until it doesn't
The most common workaround recommended in community threads is to bypass the notebook rendering pipeline entirely: write plots to DBFS as PNG files and display them with displayHTML().
```r png("/dbfs/FileStore/plots/weekly_report.png", width=800, height=600) ggplot(df, aes(x=date, y=value)) + geom_line() dev.off()
displayHTML('
')
```
This works for single plots. It fails for compound visualizations. The thread specifically called out ggarrange() from the ggpubr package — a function that composes multiple ggplot objects into a single figure. When ggarrange() writes to a manually opened PNG device on Databricks, the output is often incomplete or blank. The function manages its own internal graphics device state, and that state conflicts with the manually opened png() device.
The DBFS approach also introduces its own failure modes. The /dbfs/ FUSE mount has known reliability issues on clusters with high I/O load. Writes can silently fail or produce zero-byte files. If your notebook writes a plot to /dbfs/FileStore/plots/chart.png and the FUSE mount is degraded, displayHTML() will render a broken image tag — but the cell still succeeds.
A more robust pattern writes to the local driver node filesystem first, then copies to DBFS:
``r
png("/tmp/chart.png", width=800, height=600)
print(ggplot(df, aes(x=date, y=value)) + geom_line())
dev.off()
file.copy("/tmp/chart.png", "/dbfs/FileStore/plots/chart.png")
``
You can then verify the file was written by checking file.info("/dbfs/FileStore/plots/chart.png")$size > 0 before calling displayHTML(). This at least converts a silent failure into an explicit one you can catch.
Detect blank output before your stakeholders do
The fundamental problem is that Databricks treats plot rendering as a side effect, not a result. A cell that should produce a chart has the same exit code whether the chart renders or not. Building reliability around this requires treating rendered output as an artifact that needs verification.
For R notebooks in production, add a render-check cell after every plotting section:
``r
dev_info <- dev.list()
if (is.null(dev_info) || length(dev_info) == 0) {
stop("RENDER_FAILURE: No active graphics device. Plots in preceding cells did not render.")
}
``
This won't catch every failure — the device can exist but produce empty output — so pair it with the DBFS file-size check for critical visualizations. For ggplot objects, you can also validate the plot object itself before rendering:
``r
p <- ggplot(df, aes(x=date, y=value)) + geom_line()
if (is.null(ggplot_build(p)$data[[1]]) || nrow(ggplot_build(p)$data[[1]]) == 0) {
stop("RENDER_FAILURE: Plot contains no renderable data.")
}
print(p)
``
These checks convert silent rendering failures into job failures, which is exactly what you want. A failed job gets caught by monitoring. A succeeded job with blank output does not.
For teams running R notebooks on schedules through Databricks Jobs, MetricSign detects when a job completes but its downstream dependencies show unexpected patterns — like a report notebook that historically takes 45 seconds to render suddenly completing in 3 seconds because no plots were generated. That duration anomaly, correlated across the job's run history, surfaces as a root-cause signal before a stakeholder opens a blank notebook.
Runtime updates break R rendering with no changelog entry
The thread's timeline is telling. The problem appeared suddenly, affected all R plotting across multiple notebooks and browsers, and resolved without user intervention. This pattern points to a Databricks-side change — most likely a Runtime patch that modified the notebook server's graphics device initialization.
Databricks Runtime releases follow a cadence: major versions quarterly, maintenance patches more frequently. Maintenance patches on LTS runtimes like 14.3 can change internal components without appearing in customer-facing release notes. The graphics device pipeline for R is one of those internal components. It's not part of the Spark API surface, so changes to it don't trigger documentation updates.
This creates a monitoring blind spot. You pin your cluster to Runtime 14.3 LTS expecting stability. A maintenance patch rolls out. Your Python workloads continue normally because matplotlib uses a different rendering path. Your R notebooks silently produce empty output for two days until someone opens one.
The only defense is to treat R rendering as a first-class pipeline output. Pin your Runtime to a specific patch version when available (e.g., 14.3.x-scala2.12 rather than 14.3 LTS). Run a canary notebook that generates a known plot and verifies the output file size on a schedule. Log the Runtime version at the start of every notebook run with spark.conf.get("spark.databricks.clusterUsageTags.sparkVersion") so you can correlate rendering failures with patch changes after the fact.
Databricks SQL has 1.9% adoption in the Stack Overflow 2024 Developer Survey, reflecting the platform's relative niche status. But the teams that use it tend to use it heavily, with complex multi-language notebooks that mix SQL, Python, and R. Those teams are exactly the ones hit hardest by language-specific rendering regressions that don't affect the majority Python path.
R's second-class status on Databricks is a reliability problem
Databricks invested heavily in Python and Scala support. The Unity Catalog, MLflow integration, Delta Live Tables — all of these are Python-first. R support exists but receives less testing surface area, fewer documentation updates, and less community attention to edge cases.
Serverless compute doesn't support R at all. Shared access mode clusters restrict R functionality. The graphics rendering pipeline for R runs through a different code path than Python's matplotlib integration, which uses %matplotlib inline and IPython's display framework — a path that's tested by orders of magnitude more users.
This isn't a reason to abandon R on Databricks. Many statistical computing workflows, particularly in pharma, insurance, and academic research, depend on R packages that have no Python equivalent. The survival package, lme4 for mixed-effects models, domain-specific Bioconductor packages — these keep R notebooks in production even on platforms that treat R as an afterthought.
But it is a reason to build more defensive infrastructure around R workloads. Assume that R rendering will break periodically without warning. Assume that the fix will come from Databricks without explanation, days later. Design your notebooks so that rendering failures become job failures, and job failures become alerts.
Write critical visualizations to DBFS and verify file sizes. Add device-check cells. Log runtime versions. And route your R notebook jobs through monitoring that can distinguish between a job that completed its work and a job that completed because it skipped its work — because Databricks won't make that distinction for you.