{"id":558,"date":"2026-10-08T10:00:00","date_gmt":"2026-10-08T17:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/aspire\/?p=558"},"modified":"2026-10-08T10:00:00","modified_gmt":"2026-10-08T17:00:00","slug":"aspire-dashboard-persistence","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/aspire\/aspire-dashboard-persistence\/","title":{"rendered":"Adding persistence to the Aspire dashboard"},"content":{"rendered":"<p>Developers love the <a href=\"https:\/\/aspire.dev\/dashboard\/\">Aspire dashboard<\/a> for\ncollecting and visualizing telemetry from their apps. Structured logs help\nexplain what happened, traces show how a request moved between services, and\nmetrics reveal changes over time. Alongside telemetry, the dashboard displays\nresources, their configuration and health, and console output.<\/p>\n<p>Previously, the dashboard stored data in memory, so stopping it meant losing\nthat data. Because the dashboard starts and stops along with your AppHost,\nrestarting your app also cleared the evidence you might still need to debug it.<\/p>\n<p>Aspire 13.6 adds persistence to the dashboard. You can restart your app, return\nto an earlier run, and inspect its saved data using the same pages and filters\nyou already know. The change also lets us raise telemetry limits and reduce\nmemory usage.<\/p>\n<p>This post looks at the new experience and the engineering behind it:<\/p>\n<ul>\n<li>SQLite storage<\/li>\n<li>Dapper queries<\/li>\n<li>Keeping the dashboard responsive under load on a busy development machine<\/li>\n<\/ul>\n<h2>Benefits<\/h2>\n<h3>Revisit a previous run<\/h3>\n<p>Imagine your app fails to start correctly. You change its configuration and\nrestart. Everything now looks healthy, but you want to check the original\nfailure to understand what happened.<\/p>\n<p>The dashboard now has a run selector in its header. Open it to switch from\n<strong>Live run<\/strong> to an earlier run of the same application:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/aspire\/wp-content\/uploads\/sites\/90\/2026\/10\/aspire-dashboard-persistence-run-selector.webp\" alt=\"Expanded dashboard run selector showing the live run, previous runs, and a pinned run.\" \/><\/p>\n<p>This behavior is enabled by default when an AppHost launches the dashboard.\nEach time the dashboard starts, it creates a new run with its own SQLite\ndatabase. Selecting an earlier run lets you inspect its saved resource state,\nlogs, traces, and metrics. You don&#8217;t need to reproduce the failure just to see\nits telemetry again.<\/p>\n<p>See <a href=\"https:\/\/aspire.dev\/dashboard\/explore\/#dashboard-runs\">dashboard runs<\/a>\nfor the full walkthrough.<\/p>\n<h3>Higher telemetry limits<\/h3>\n<p>Keeping telemetry in memory required conservative limits. A busy app can\nproduce a lot of logs, and the dashboard shares your machine with an editor,\nagents, builds, containers, and the app you&#8217;re developing.<\/p>\n<p>Storing that history in SQLite instead of keeping it all in memory lets us\nraise default telemetry limits:<\/p>\n<table>\n<thead>\n<tr>\n<th>Data type<\/th>\n<th>Previous default<\/th>\n<th>Aspire 13.6 default<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Console log entries<\/td>\n<td>10,000<\/td>\n<td>100,000<\/td>\n<\/tr>\n<tr>\n<td>Structured logs<\/td>\n<td>10,000<\/td>\n<td>100,000<\/td>\n<\/tr>\n<tr>\n<td>Traces<\/td>\n<td>10,000<\/td>\n<td>100,000<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>You can increase these limits further through\n<a href=\"https:\/\/aspire.dev\/dashboard\/configuration\/#telemetry-limits\">configuration<\/a>.\nOur testing during development showed that the dashboard remained usable and\nresponsive with millions of rows of telemetry.<\/p>\n<h3>Reduced memory usage<\/h3>\n<p>SQLite lets the dashboard retrieve telemetry as needed instead of keeping\nthe entire history as .NET objects.<\/p>\n<p>In a benchmark that loaded the same large volume of logs, traces, and metrics\ninto both versions, private memory usage fell from <strong>1,007 MB in Aspire 13.5 to\n241 MB in Aspire 13.6<\/strong>, a <strong>76% reduction<\/strong>.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/aspire\/wp-content\/uploads\/sites\/90\/2026\/10\/aspire-dashboard-persistence-memory.webp\" alt=\"Private memory after forced GC: Aspire 13.5 uses 1,007.38 MB; Aspire 13.6 uses 240.96 MB. One run per version.\" \/><\/p>\n<h2>SQLite and Dapper under the hood<\/h2>\n<p>Persistence needed to fit the dashboard&#8217;s role as a development tool. We\ndidn&#8217;t want to ask developers to install and manage another database server\njust to inspect their applications. We also needed to keep filtering and\npaging responsive as the amount of stored telemetry grew.<\/p>\n<p><a href=\"https:\/\/sqlite.org\/\">SQLite<\/a> fits that workload well. It&#8217;s a fast, embedded\ndatabase that runs inside the dashboard process. There is no separate server\nto configure or network connection between the dashboard and its database.\nThe dashboard ships the dependencies it needs.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/aspire\/wp-content\/uploads\/sites\/90\/2026\/10\/aspire-dashboard-persistence-data-flow.webp\" alt=\"Telemetry is stored in SQLite; change notifications refresh the live dashboard UI using SQL and Dapper queries.\" \/><\/p>\n<h3>High-performance SQL<\/h3>\n<p>We use <a href=\"https:\/\/github.com\/DapperLib\/Dapper\">Dapper<\/a> to execute tuned SQL\nand map query results to .NET objects.<\/p>\n<p>We considered Entity Framework Core, but for this data layer we wanted direct\ncontrol over the SQL and how Dapper materializes results. A dashboard query\nmight filter by resource, severity, text, and attributes, count the matches,\nand then retrieve one page in a particular order. Those queries are central\nto the experience, and we wanted to tune them directly.<\/p>\n<p>For example, the\n<a href=\"https:\/\/github.com\/microsoft\/aspire\/blob\/v13.6.0\/src\/Aspire.Dashboard\/Otlp\/Storage\/SqliteTelemetryRepository.Logs.cs\">structured-log query<\/a>\nfilters, counts, sorts, and pages results in SQLite before materializing the\nrequested logs. Looking at a page of results doesn&#8217;t require loading all\nretained logs and filtering them in .NET.<\/p>\n<p><a href=\"https:\/\/aot.dapperlib.dev\/\">Dapper.AOT<\/a> also generates query and mapping code\nat build time instead of relying on runtime code generation. We use that\nsupport in the Native AOT dashboard in Aspire 13.6.<\/p>\n<p>Fun fact: we used the Aspire dashboard to profile its own SQL queries during\ndevelopment.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/aspire\/wp-content\/uploads\/sites\/90\/2026\/10\/aspire-dashboard-persistence-sql-profiling.webp\" alt=\"Aspire dashboard profiling its own SQLite operations, with a trace waterfall and SQL span details.\" \/><\/p>\n<h2>Protecting persisted data<\/h2>\n<p>Resource configuration and telemetry can contain secrets. Saving them to disk\nmeans we need to think about who can read that disk, not just who can sign in\nto the dashboard.<\/p>\n<p>By default, persistent data lives under the <code>dashboard<\/code> directory in the\ncurrent user&#8217;s <code>.aspire<\/code> directory. Setting <code>ASPIRE_HOME<\/code> changes that base\nlocation, and the dashboard also supports an explicit data-directory setting.\nKeeping the default under the user profile avoids writing sensitive data\ninto a source directory you might commit or share.<\/p>\n<p>See the <a href=\"https:\/\/aspire.dev\/dashboard\/security-considerations\/#protect-persisted-data\">guidance on protecting persisted data<\/a>\nfor more information.<\/p>\n<h2>Configure standalone persistence<\/h2>\n<p>The dashboard is also useful\n<a href=\"https:\/\/aspire.dev\/dashboard\/standalone\/\">without an AppHost<\/a>, including for\napplications written in other languages. Persistence has three modes to\nsupport those different workflows:<\/p>\n<table>\n<thead>\n<tr>\n<th>Mode<\/th>\n<th>What the dashboard does on restart<\/th>\n<th>Run selector<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>None<\/code><\/td>\n<td>Creates a new temporary database; doesn&#8217;t retain previous data<\/td>\n<td>No<\/td>\n<\/tr>\n<tr>\n<td><code>Run<\/code><\/td>\n<td>Creates a new database and retains previous runs<\/td>\n<td>Yes<\/td>\n<\/tr>\n<tr>\n<td><code>Resume<\/code><\/td>\n<td>Reopens the same application database<\/td>\n<td>No<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>An AppHost sets the mode to <code>Run<\/code> automatically. The standalone dashboard\ndefaults to <code>None<\/code>, so keeping its data is an explicit choice. For a standalone\ndashboard that retains its history across restarts, use:<\/p>\n<pre><code class=\"language-text\">aspire dashboard run --application-name my-app --persistence Resume<\/code><\/pre>\n<h3>Keep data outside a container<\/h3>\n<p>A popular way to use the Aspire dashboard is to run its standalone container.\nA database inside a disposable container doesn&#8217;t help when that container is\nremoved. The solution is to mount persistent storage and configure the dashboard\ninside the container to use it.<\/p>\n<p>The <a href=\"https:\/\/aspire.dev\/dashboard\/standalone\/#persist-data-between-container-runs\">standalone container guide<\/a>\nshows the volume mount and container setup. See\n<a href=\"https:\/\/aspire.dev\/dashboard\/configuration\/#data-persistence\">dashboard configuration<\/a>\nfor the corresponding configuration keys and defaults.<\/p>\n<h2>Try it in Aspire 13.6<\/h2>\n<p>Aspire dashboard data persistence is available now!\n<a href=\"https:\/\/aspire.dev\/whats-new\/aspire-13-6\/\">Upgrade your Aspire CLI and AppHost packages to 13.6<\/a>\nto get dashboard run history by default. For standalone use, choose <code>Run<\/code> or\n<code>Resume<\/code> as described above.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Aspire 13.6 adds dashboard persistence and run history. Revisit telemetry after a restart, retain more data, and explore the SQLite and Dapper implementation behind it.<\/p>\n","protected":false},"author":11402,"featured_media":559,"comment_status":"open","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1,17],"tags":[9,88,53,56,87],"class_list":["post-558","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-aspire-category","category-deep-dives","tag-aspire","tag-dapper","tag-dashboard","tag-opentelemetry","tag-sqlite"],"acf":[],"blog_post_summary":"<p>Aspire 13.6 adds dashboard persistence and run history. Revisit telemetry after a restart, retain more data, and explore the SQLite and Dapper implementation behind it.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/posts\/558","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/users\/11402"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/comments?post=558"}],"version-history":[{"count":2,"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/posts\/558\/revisions"}],"predecessor-version":[{"id":564,"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/posts\/558\/revisions\/564"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/media\/559"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/media?parent=558"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/categories?post=558"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/tags?post=558"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}