What Is a PDB (Program Database)? — Understanding Debug Information, Symbols, and Source Link

· · .NET, C#, Visual Studio, PDB, Debugging, Symbols, SourceLink, Diagnostics, Operations, Legacy Asset Reuse

1. The First Thing to Understand

When you build a .NET or C++ application, a .pdb file is sometimes generated alongside the .dll or .exe.

For example, output like this:

MyApp.exe
MyApp.dll
MyApp.pdb

If you keep developing without knowing what this .pdb is, questions like these come up:

  • Is it OK to put .pdb files in production?
  • Does the application fail to run without the .pdb?
  • Is it wrong that a Release build produces a .pdb?
  • Does the .pdb contain the entire source code?
  • Does having a .pdb guarantee you can set breakpoints?
  • Why is a .pdb needed for dump analysis and failure investigation?
  • How should .pdb files be handled in NuGet packages?
  • What is the difference between Source Link, symbol servers, and .snupkg?

PDBs are never the star of day-to-day implementation work. But for failure investigation, dump analysis, library distribution, readable operational logs, and the debugging experience, they matter a great deal.

Let’s state the conclusion up front.

A PDB is a debug information file that connects an executable or assembly to its source code. It is not the application itself; it tells debuggers and diagnostic tools “which instruction corresponds to which source line,” “what each local variable is,” and “which source to look at.”

This article treats the PDB not as a mere “debug-only extra file,” but as a build artifact you should manage deliberately in real-world work.

2. What Is a PDB?

PDB stands for Program Database. .pdb files are also frequently called symbol files.

Symbols, roughly speaking, are information about names and locations in a program. For example:

  • Function names
  • Method names
  • Local variable names
  • Parameter names
  • Type information
  • Source file names
  • Source line numbers
  • The mapping between source locations and compiled instructions
  • Information debuggers use to place breakpoints
  • Source retrieval information for Source Link

While you are writing source code, you have human-readable names like these:

public decimal CalculateTotalPrice(Order order)
{
    var subtotal = order.Lines.Sum(x => x.Price * x.Quantity);
    var tax = subtotal * 0.10m;
    return subtotal + tax;
}

But the built .dll or .exe is not the source code itself. For .NET it becomes IL plus metadata; for native C++ it becomes a binary close to machine code.

As a result, information like the following becomes hard or impossible to determine from the executable alone:

Which line of which .cs file does this machine code / IL correspond to?
Which position in which function is this address?
What was the name of this local variable?
At which instruction position should this breakpoint actually be placed?
Which source does this stack frame correspond to?

The PDB is the file that bridges this gap.

3. Is a PDB Required to Run?

Normally, a PDB is not required to run the application. With the .dll or .exe in place, the application can start. The absence of a .pdb does not prevent normal processing from executing.

Without the PDB, however, things like these become difficult:

What you want to do The problem without a PDB
Step through code precisely in Visual Studio Source lines cannot be mapped to execution positions
Set breakpoints The corresponding instruction position is unknown, so breakpoints may remain unresolved
Show file names and line numbers in exception stack traces Line number information is missing or incomplete
Analyze a dump file Reading stacks, variables, and types becomes hard
Step into an external library The library cannot be tied back to its source
Read a native crash You only get addresses, with no function names or locations

In other words, the PDB is not a “file for running” but a “file for investigating.”

This distinction matters. When a production incident occurs, the application may have been running fine without the PDB — but the investigator is stuck without it. So regardless of whether you deploy PDBs to the runtime environment, you must always retain them as build artifacts.

4. What Do You Gain from a PDB?

With a PDB, debuggers and diagnostic tools can more easily turn binaries back into human-readable information.

For example, crash information without a PDB can look like this:

MyApp.dll!0x00007ff9a1234567
MyApp.dll!0x00007ff9a1234abc
MyApp.dll!0x00007ff9a1234def

When the PDB loads correctly, you can see this much:

MyApp.Services.OrderService.CalculateTotalPrice(Order order) Line 42
MyApp.Controllers.OrderController.Post(CreateOrderRequest request) Line 87
MyApp.Program.Main(string[] args) Line 16

The difference is enormous.

With the former, you have to start the investigation from raw addresses. With the latter, you start at “which line of which method.”

In failure investigation, whether you can close in on the cause within the first 30 minutes matters. Simply having the PDB completely changes the starting point of the investigation.

5. What Is in a PDB

The information in a PDB varies by language, compiler, PDB format, and build settings. So you cannot simply say “a PDB always contains exactly this.”

That said, .NET developers typically expect roughly this information:

The mapping between source files and compiled code
Source line numbers
Method and function symbols
Local variable names
Scope information
Source file paths and checksums
Source Link information
In some cases, embedded source

The most important part is the mapping between source locations and runtime locations.

A single line of C# may become multiple instructions in IL or in the JIT-compiled native code. Conversely, optimization may merge multiple source lines, eliminate them, or make them appear reordered.

The debugger uses the PDB’s information to decide “which source line to display right now.”

6. What Is Not in a PDB

With PDBs, understanding what is not in them dispels more misconceptions than understanding what is.

A PDB is normally not:

  • The application itself
  • A runtime file required for execution
  • A complete backup of the full source tree
  • A substitute for the Git repository
  • Something that fully restores every build setting and environment detail
  • Something that automatically explains the cause of a bug by itself

There is a caveat, however.

A PDB can contain source file paths, type names, function names, local variable names, and in some cases Source Link information or embedded source.

So you cannot say “a PDB is not the source code itself, so it can be published without a second thought.”

It may contain internal project names, paths including user names, internal directory structures, unreleased type names, and names from which business logic can be inferred.

7. Common Misconception 1: A PDB Slows Down Production

Merely having the PDB sitting next to the binary does not slow down normal application processing.

The PDB is used when a debugger or diagnostic tool needs symbol information. The application’s normal processing does not run while reading the PDB on every operation.

Of course, it is used when resolving file names and line numbers in exception stack traces, when attaching a debugger, or when a profiler or diagnostic tool reads symbols.

But the understanding that “deploying a PDB makes everything permanently slower, so it must never go to production” is sloppy.

In practice, this is the safe way to think about it:

There is little reason to delete PDBs purely for runtime performance
Decide placement based on exposure scope, information leakage, artifact size, and operational policy
Even if you do not deploy them, always retain the PDBs from the same build

8. Common Misconception 2: Release Builds Don’t Need PDBs

PDBs are useful for Release builds too. In fact, what you need for investigating production incidents is the Release build’s PDB.

If what runs in production is the Release build, having the Debug build’s PDB is useless. What the debugger needs is the PDB generated when that production binary was built.

The important distinction is this:

Item Meaning
Debug / Release The build configuration: optimization, conditional compilation, output settings, etc.
PDB presence Whether debug information is generated and retained
Debuggability Determined by optimization, PDB content, source matching, JIT behavior, etc.

Release builds are usually optimized, so stepping through them is less clear than Debug builds. Local variables may be optimized away, and execution may not stop in source-line order.

Even so, with a PDB, information like this becomes much easier to obtain:

  • The source line where an exception occurred
  • Method names on the stack
  • Corresponding locations during dump analysis
  • Function names in profiling results
  • Cross-referencing logs against source

It is not “Release, therefore no PDB needed.” It is precisely because it is Release that you must keep the PDB corresponding to that build.

9. Common Misconception 3: With a PDB, You Can Debug Any Binary

A PDB cannot be reused against any arbitrary .dll or .exe.

The debugger verifies that the target binary and the PDB match. If you force a mismatched PDB, the mapping of source lines, functions, and variables goes wrong.

For example, in situations like these, the PDB may be unusable or useless even though you have one:

You are trying to apply a locally rebuilt PDB to the production DLL
Same version number, but actually built from a different commit
You are trying to load a pre-hotfix PDB against a post-hotfix DLL
Optimization settings or conditional compilation differ

Do not think “same source, so it roughly fits” — think “the PDB must correspond to the exact same build artifact.”

That is why, in CI/CD, the basic unit of retention is this:

Commit ID
Build number
Artifact version
.dll / .exe
.pdb
Source reference information

Keeping this combination intact is what matters.

10. Common Misconception 4: With a PDB, You Can Fully Read Code Without Source

Even with a PDB, the source code is not necessarily inside it.

A PDB primarily holds information that maps source to binary. It can hold source file paths, checksums, and Source Link information, but an ordinary PDB does not always carry the full source text.

So if you want to step into an external library in the debugger, you need one of the following:

You have the same source files locally
Source Link can fetch the source for the correct commit
The source is embedded in the PDB
You substitute decompiled source

Visual Studio also has a feature that decompiles and displays .NET assemblies. But decompiled output is not the original source. Comments, whitespace, local variable names, the original style, and preprocessor conditions are lost or altered.

It is handy for investigation, but do not over-rely on it as a substitute for the original source.

11. PDBs and Stack Traces

In .NET exception stack traces, what you see changes depending on whether a PDB is present.

Even without a PDB, method names and type names may still appear, because .NET assemblies contain metadata.

But if you want file names and line numbers, the PDB becomes important.

For example, without a PDB, stack traces tend to look like this:

System.InvalidOperationException: Order is invalid
   at MyApp.Services.OrderService.Validate(Order order)
   at MyApp.Controllers.OrderController.Post(CreateOrderRequest request)

When the PDB is present and line numbers resolve, it changes to this:

System.InvalidOperationException: Order is invalid
   at MyApp.Services.OrderService.Validate(Order order) in /src/MyApp/Services/OrderService.cs:line 42
   at MyApp.Controllers.OrderController.Post(CreateOrderRequest request) in /src/MyApp/Controllers/OrderController.cs:line 87

When this difference shows up during operations, investigation speed changes dramatically.

Note, however, that exposing file paths can itself be information disclosure. You also need separate countermeasures: do not include stack traces in externally returned error responses, and restrict where logs are stored.

12. PDBs and Dump Analysis

The moment you appreciate PDBs most is during dump analysis.

Suppose problems like these occur in production:

  • The process crashed
  • CPU usage stayed pegged high
  • It looks like a deadlock
  • Memory keeps growing
  • No responses are coming back
  • It crashes at the boundary with a native library

You capture a dump file and analyze it.

But the dump alone is not enough. What the dump contains is the process state at that moment. To convert the stacks and modules captured there into human-readable names and source lines, you need the corresponding PDBs.

For .NET, you analyze using dotnet-dump, Visual Studio, WinDbg, SOS, and so on. When native code is involved, WinDbg symbol configuration becomes important.

Common failures in dump analysis include:

The production DLL remains, but there is no PDB
There is a PDB, but it was a different one rebuilt locally
Windows / .NET runtime symbols are not being loaded
You have your own app's PDBs but no symbols for third-party libraries
The symbol path is not configured, so the debugger cannot find the PDBs

For dump analysis, preparing after the incident is sometimes too late. Retaining PDBs at build time and being able to retrieve them at investigation time is essential.

13. Windows PDB and Portable PDB

PDBs come in multiple formats.

The two representative ones to know in practice are:

Type Main context Characteristics
Windows PDB Visual C++, traditional Windows debugging The format commonly used in Windows native development
Portable PDB .NET / .NET Core and later A .NET-oriented format usable cross-platform

For .NET Core and later, the Portable PDB is the important one. The Portable PDB format can be handled not only on Windows but also on Linux and macOS.

In .NET Framework-era projects and older Visual Studio configurations, you will still encounter Windows PDBs. In current .NET SDK-style projects, the working assumption is increasingly the Portable PDB.

One thing to note: both formats use the .pdb extension.

You cannot tell the format from the extension alone. When someone says “PDB,” confirm which context they mean:

A .NET Portable PDB?
A Visual C++ Windows PDB?
An old .NET Framework project?
A PDB for NuGet distribution?
Symbols used in WinDbg?

14. DebugType in .NET

In C# projects, DebugType specifies how debug information is emitted.

The representative values are:

DebugType Meaning
portable Generates a Portable PDB as a separate file
embedded Embeds Portable PDB-equivalent debug information into the .dll / .exe
full Generates a PDB in the current platform’s default format
pdbonly Effectively no different from full in C# 6.0 and later
none Generates no PDB

In current .NET SDK-style projects, C#’s DebugType defaults to portable for both Debug and Release. So you normally do not need to specify DebugType just to get a Portable PDB.

Specifying it explicitly is worthwhile when you want to declare “we lock this format in” as a project or organization, when you want differences from older projects to be visible, or when you choose a non-default policy such as embedded / none.

For NuGet libraries and external distribution, consider whether to use portable, embedded, or .snupkg.

For example, to explicitly produce a Portable PDB:

<PropertyGroup>
  <DebugType>portable</DebugType>
</PropertyGroup>

To embed the PDB into the assembly instead of a separate file:

<PropertyGroup>
  <DebugType>embedded</DebugType>
</PropertyGroup>

If you absolutely do not want a PDB from Release builds, you can write this:

<PropertyGroup Condition="'$(Configuration)' == 'Release'">
  <DebugType>none</DebugType>
</PropertyGroup>

But decide this carefully. Configuring Release builds to emit no PDB may leave you stranded in your own production failure investigations.

15. Is DebugSymbols=false Enough?

When people want to stop PDB generation, you sometimes see DebugSymbols set to false:

<PropertyGroup Condition="'$(Configuration)' == 'Release'">
  <DebugSymbols>false</DebugSymbols>
</PropertyGroup>

But if the intent is to reliably suppress PDB generation, setting DebugType to none is clearer:

<PropertyGroup Condition="'$(Configuration)' == 'Release'">
  <DebugType>none</DebugType>
</PropertyGroup>

This setting does get used as a distribution policy for libraries and applications. But to repeat: not producing PDBs degrades your failure investigation capability.

In practice, this division is common:

Always generate PDBs as build artifacts
Decide separately whether to deploy them to production servers
Even if not deployed, retain them in CI artifacts or a symbol server

16. C++ PDBs Are a Bit Different from .NET PDBs

C++ PDBs live in a slightly different context from .NET PDBs.

In Visual C++, PDBs are generated by options such as /Zi and /ZI. Both the PDB used by the compiler and the PDB the linker produces for the final .exe / .dll are involved.

In C++, PDBs are crucial for reading native code addresses, functions, types, local variables, inline expansion, and post-optimization locations.

In native failure investigations, the situation without a PDB tends to look like this:

You know the exception address
You know the module name
But you don't know the function name or source line

For applications that mix C++ and C#, P/Invoke, C++/CLI, and .NET apps calling native DLLs, you need not just the .NET-side PDBs but the native-side PDBs as well.

17. Public Symbols and Private Symbols

In the world of Windows symbols, there is a distinction between public symbols and private symbols.

Roughly, the difference is:

Type Rough idea of contents
private symbols Nearly complete information including local variables, types, parameters, detailed internals
public symbols Information trimmed for publication, such as function names and addresses

For symbols distributed externally, private symbols are sometimes stripped, leaving only public symbols.

This balances debuggability against the scope of information disclosure.

For example: you want to expose minimal function names so customers’ crashes in your product can be analyzed, but you do not want internal local variable names and type information exposed. In such cases, stripped PDBs are used.

In Windows-oriented native development, tools such as PDBCopy are used to produce PDBs with private symbols removed.

Meanwhile, for internal failure investigation, you must retain the full PDBs. If only the externally trimmed PDBs survive, deep investigations will suffer.

18. Where Do Debuggers Look for PDBs?

Visual Studio and WinDbg search several locations for PDBs.

The typical ones are:

The project's output folder
The same folder as the .dll / .exe
The original PDB path recorded inside the .dll / .exe
Folders specified in Visual Studio's symbol settings
The local symbol cache
An internal symbol server
Microsoft Symbol Server
NuGet.org Symbol Server
Symbol servers such as Azure Artifacts

If a PDB exists but is not loading, check these points in order:

Does the PDB match the target binary?
Is the PDB on the debugger's search path?
Can the symbol server be reached?
Is a stale copy sitting in the local cache?
Is symbol loading disabled for the target module?
Is it being treated as external code by the Just My Code setting?

In Visual Studio, the Modules window during debugging shows the symbol load state of each module.

Debug
  Windows
    Modules

There, look at the Symbol Status of the target DLL.

The four common statuses are:

Symbols loaded.
Cannot find or open the PDB file.
PDB does not match image.
Skipped loading symbols.

When stuck on anything PDB-related, looking at the Modules window first is the shortest path.

19. What Is a Symbol Server?

A symbol server is a mechanism that lets debuggers fetch symbol files such as PDBs when they need them.

Simply dropping PDBs into a shared folder works to a degree. But as versions multiply, it breaks down quickly.

PDB for MyApp v1.0.0
PDB for MyApp v1.0.1
PDB for MyApp v1.0.1 hotfix
PDB for MyApp v1.1.0-beta
PDB with different settings just for customer A

You end up with dozens of files all named MyApp.pdb.

A symbol server organizes PDBs not by mere file name but by their binary-matching information. That makes it easy for the debugger to find “the PDB that fits this DLL.”

An example of a practical setup:

Microsoft Symbol Server
  Used to fetch symbols for Windows, the .NET runtime, etc.

NuGet.org Symbol Server
  Used to fetch symbols for public NuGet packages

Internal symbol server
  Stores PDBs for your own apps and libraries

Local symbol cache
  Reuses previously fetched PDBs to speed up debugging

If you investigate production incidents in internal services, having CI publish PDBs to an internal symbol store is very convenient.

Source Link is a mechanism that connects PDBs to the source control system.

Even if the PDB knows “this location corresponds to this source file,” the debugger cannot display it if that source file is not on hand.

With Source Link, the debugger can use information embedded in the PDB to fetch the source files for the corresponding commit from GitHub, Azure Repos, GitLab, Bitbucket, and so on.

In other words, Source Link solves this problem:

You want to step into a library obtained via NuGet
You haven't cloned that library's source locally
But you have the PDB and the repository information
The debugger goes and fetches the source for the correct commit

This dramatically improves the experience for library consumers.

The key point of Source Link is that it references not “the latest main branch” but “the commit that produced that binary.”

Being tied to the source as of build time — not the latest source — is what makes it meaningful.

In the .NET 8 SDK and later, the handling of Source Link has improved.

For commonly used providers — GitHub, Azure Repos, GitLab, Bitbucket — the Source Link machinery is now included in the .NET SDK itself.

So the old understanding that you must always explicitly add Microsoft.SourceLink.GitHub and the like is becoming outdated.

There are still cases that require checking, though:

You are building with an SDK older than .NET 8
It is an old, non-SDK-style project
You use self-hosted, custom Git hosting
You use a Source Link provider outside the standard set
You want the NuGet package metadata fully in order as well

To publish repository information in the NuGet package, use this setting:

<PropertyGroup>
  <PublishRepositoryUrl>true</PublishRepositoryUrl>
</PropertyGroup>

If you need to embed untracked files into the PDB, consider this setting:

<PropertyGroup>
  <EmbedUntrackedSources>true</EmbedUntrackedSources>
</PropertyGroup>

Note that embedding affects the scope of information disclosure. What goes into the PDB must be decided according to the audience and operational policy.

22. What Is an Embedded PDB?

When you specify embedded for DebugType, the Portable PDB debug information is embedded into the .dll or .exe. In this case, no separate .pdb file is generated.

<PropertyGroup>
  <DebugType>embedded</DebugType>
</PropertyGroup>

This is convenient in scenarios like:

  • You want to distribute something close to a single file
  • You want to avoid forgetting to ship the PDB
  • A small internal tool where you want debug information bundled
  • You want to reduce the hassle of PDB distribution for NuGet packages

There are downsides, however:

  • The assembly size increases
  • The distributed artifact always contains debug information
  • Control over information disclosure becomes coarser
  • For large libraries, restore and distribution sizes are affected

Embedded PDBs are convenient, but “just make everything embedded” is not the answer.

For externally published libraries in particular, weigh which is better: a symbol package via .snupkg, Source Link, bundling regular PDBs, or embedded.

23. What Is .snupkg?

.snupkg is NuGet’s symbol package format.

A regular NuGet package is .nupkg. A symbol package is .snupkg.

MyLibrary.1.2.3.nupkg
MyLibrary.1.2.3.snupkg

The .nupkg contains the library itself that consumers reference. The .snupkg is used to distribute the PDBs for debugging.

The important point here is that .snupkg is fundamentally for managed-code Portable PDBs. At least on the NuGet.org symbol server, only Portable PDBs are supported; Windows PDBs produced by native projects such as C++ are not accepted. If you need to distribute or retain Windows PDBs, consider other channels: the legacy .symbols.nupkg, an internal symbol server, or CI artifacts.

To create one, for example:

<PropertyGroup>
  <IncludeSymbols>true</IncludeSymbols>
  <SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>

You can also specify it on the command line:

dotnet pack -c Release -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg

For public NuGet libraries, using .snupkg plus Source Link strikes a better balance between distribution size and debugging experience than packing the PDBs into the main .nupkg.

But watch the support status of feeds and tooling. If your internal NuGet feed does not support .snupkg, you will need another approach.

24. Should PDBs Go to Production?

“Should PDBs be deployed to production?” is not a simple yes / no.

There are four decision axes:

Ease of failure investigation
Information disclosure risk
Distribution size
Operational rules

For internal systems, deploying the .pdb alongside the .dll in the same folder is a realistic practice. Line numbers appear more readily in exception logs, and dump analysis gets easier.

For externally distributed applications, think carefully before bundling PDBs as-is. Internal structures, local variable names, and source paths may become visible. If needed, take options such as trimming to public symbols, going through a symbol server, or merely retaining them for support purposes.

For web services, beyond whether to place PDBs on the server, also consider how stack traces appearing in logs are handled. Stack traces should not be returned in external responses. You may keep them in internal logs, but decide on access permissions and retention periods.

Our practical recommendation:

Always generate PDBs
Retain PDBs as build artifacts
Decide production placement according to the system's exposure scope
If publishing externally, review the scope of disclosed information
Keep them somewhere retrievable for dump analysis

25. Are PDBs Confidential?

PDBs do not necessarily require the same handling as the source code itself or private keys. But treating them as harmless files is also dangerous.

Here are things a PDB can potentially reveal:

  • Developers’ local paths
  • Internal folder structures
  • Project names
  • Class and method names
  • Local variable names
  • Internal API names
  • Business terminology
  • Source Link repository URLs
  • Embedded source
  • Source Server configuration

In particular, with the older Source Server mechanism and some native PDB features, the debugger may execute commands to fetch source. Avoid using untrusted PDBs or symbol servers unconditionally.

Care is also needed when feeding untrusted PDBs into tools and libraries that parse them. For systems that automatically process externally received PDBs, design with untrusted input in mind.

In summary, the safe handling is:

Protect full internal PDBs as internal artifacts
Review the contents and audience of any externally published PDBs
If the Source Link target is a private repository, manage authentication and permissions
Do not register untrusted symbol servers in your debugger

26. PDB Retention Policy in CI/CD

PDBs that survive only on a developer’s local PC are meaningless. What you need when a production incident happens is the PDB corresponding to the build that was actually released.

So in CI/CD, retain them in a form like this:

Build number: 2026.06.10.1234
Commit ID: abcdef123456...
Artifacts:
  MyApp.dll
  MyApp.pdb
  MyApp.deps.json
  MyApp.runtimeconfig.json
  package.zip
  container image digest

Ideally, also tie in this information:

Git commit
Git tag
Release number
Environment name
Build configuration
Target framework
RID
Container image digest
NuGet lock file

What matters is not retaining PDBs in isolation, but never losing track of which binary each PDB corresponds to.

An operation that keeps overwriting a single MyApp.pdb on a file share will break eventually. Make them retrievable per build, per version, per commit.

27. PDB Placement Patterns

There are several practical patterns for handling PDBs.

Pattern 1: Place the PDB in the Same Folder as the DLL

The simplest.

publish/
  MyApp.dll
  MyApp.pdb

The advantage is simple configuration. Debuggers and the runtime find it easily, and line numbers appear readily in exception logs.

The downside is that the distributed artifact contains debug information. It may not suit external distribution.

Pattern 2: Don’t Deploy PDBs; Retain Them in CI Artifacts

Keep PDBs off the production servers and retain them in CI artifacts.

release-artifacts/
  app.zip
  symbols.zip

When a production incident occurs, capture dumps and logs, retrieve the PDBs for the corresponding build number, and analyze.

It limits information exposure, but requires a retrieval procedure at investigation time.

Pattern 3: Publish to an Internal Symbol Server

For large teams, this is the most manageable.

CI publishes PDBs to the symbol store at build time. Developers’ and investigators’ Visual Studio / WinDbg reference that symbol server.

The advantage is safe handling of PDBs across many versions. The disadvantage is the initial setup and access control required.

Pattern 4: Distribute via NuGet .snupkg

Strong for public libraries.

MyLibrary.1.2.3.nupkg
MyLibrary.1.2.3.snupkg

Consumers restore only the regular package, and only the symbols needed at debug time are fetched.

Combined with Source Link, stepping into source becomes easy even for external libraries.

Pattern 5: Use Embedded PDBs

Convenient for avoiding forgotten PDBs.

<PropertyGroup>
  <DebugType>embedded</DebugType>
</PropertyGroup>

But watch the assembly size and the scope of information disclosure.

28. What to Check First in an Existing Project

If you are reviewing PDB handling in an existing .NET project, start with these checks:

Are PDBs generated in Release builds?
Where are the generated PDBs stored?
Are the DLLs shipped to production retained together with their matching PDBs?
Can PDBs be retrieved at dump analysis time?
Is Source Link enabled?
For NuGet libraries, are you publishing .snupkg?
Are you including more in the PDB than necessary?

In the csproj, check settings like these:

<PropertyGroup>
  <TargetFramework>net8.0</TargetFramework>
  <DebugType>portable</DebugType>
  <PublishRepositoryUrl>true</PublishRepositoryUrl>
  <EmbedUntrackedSources>true</EmbedUntrackedSources>
  <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>

For NuGet packages, these are also candidates:

<PropertyGroup>
  <IncludeSymbols>true</IncludeSymbols>
  <SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>

That said, putting identical settings into every project is not the answer. The optimal choice differs for internal apps, external libraries, on-premises products, SaaS, and OSS.

29. How to Diagnose PDBs That Won’t Load

When a PDB will not load, triage step by step rather than rebuilding on a hunch.

1. Check the Target Module

In Visual Studio’s Modules window, find the target DLL / EXE.

Debug > Windows > Modules

The columns to look at are:

Module
Path
Symbol Status
Symbol File
Version
Timestamp

2. Read the Symbol Status

Each status reads roughly as follows:

Status Meaning
Symbols loaded Loaded
Cannot find or open the PDB file The PDB cannot be found
PDB does not match image A PDB exists but does not match the target binary
Skipped loading symbols Possibly not loaded due to settings

3. Verify Your Local PDB Is from the Same Build

A common mistake is using a PDB rebuilt locally.

Even with the same source, different build conditions can produce a mismatch. Retrieve the exact PDB shipped to production from the CI artifacts.

4. Check the Symbol Path

In Visual Studio, check here:

Tools > Options > Debugging > Symbols

In WinDbg, check with, for example:

.sympath
.reload
!sym noisy

If you use Microsoft’s public symbols, specifying a local cache is convenient:

srv*C:\Symbols*https://msdl.microsoft.com/download/symbols

5. Suspect the Cache

You may be holding a stale PDB or a corrupted cache. Clear the symbol cache, specify a different cache, or examine the verbose load logs.

30. PDBs and “Just My Code”

Visual Studio has a setting called Just My Code.

It narrows the debugging target to “your code,” making external code harder to step through. It is convenient for everyday development, but a source of confusion when verifying PDBs or Source Link.

For example, if an external NuGet library has a PDB and Source Link but you cannot step in, check these:

Is Just My Code enabled and treating it as external code?
Is Enable Source Link support enabled?
Is the NuGet.org Symbol Server enabled?
Does the target package publish a PDB / .snupkg?
Can the source retrieval target be reached?

Before declaring “the PDB is broken,” check the debugger settings, too.

31. PDBs and Decompilation

Recent versions of Visual Studio can decompile .NET assemblies and use the result for debugging.

This lets you trace external libraries to some extent even without PDBs or source.

But decompilation is not a panacea:

Original comments do not come back
Original whitespace and structure do not come back
Local variable names can change
async / iterators / pattern matching can look different from the original
Mapping is hard to follow in optimized code

If you have the PDB and Source Link, using them is generally the natural approach. Think of decompilation as “a fallback for when there is no PDB or source.”

32. Log Design and PDBs

PDBs also relate to log design.

For example, exception logs with file names and line numbers are easier to investigate. But relying on logs alone is dangerous.

In production incidents, things like these happen:

The line number in the log doesn't match the current main branch
A hotfix was redeployed under the same version number
The PDB wasn't retained, so the line number's meaning can't be verified
The container image remains, but no one knows the corresponding source commit

So emit not just line numbers in logs but build information as well:

ApplicationVersion: 1.8.3
GitCommit: abcdef1234567890
BuildNumber: 20260610.12
Environment: Production

When PDBs, source, logs, dumps, and deployment history connect, failure investigation becomes far easier.

33. PDBs in Container Operations

When running .NET apps in containers, you need to make PDB handling explicit.

For example, whether to include PDBs in the Docker image:

FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY publish/ .
ENTRYPOINT ["dotnet", "MyApp.dll"]

If publish/ contains the PDBs, they go into the image as-is.

This has benefits:

  • Line numbers appear readily in stack traces inside the container
  • They are easy to find on the same file system when capturing dumps
  • Cross-referencing during investigation is simple

There are concerns, too:

  • The image size increases
  • Debug information is included in the production image
  • If the image is distributed externally, the scope of disclosure widens

For an internal-only SaaS, including PDBs in the image is a perfectly reasonable choice. For an on-premises product handed to external customers, retaining PDBs separately may be better.

Either way, even if you do not include PDBs in the image, retaining the corresponding PDBs as artifacts is mandatory.

34. Single-File Publishing and PDBs

.NET supports single-file publishing.

dotnet publish -c Release -r win-x64 -p:PublishSingleFile=true

Here too, you need to confirm how debug information is handled.

Going single-file does not make failure investigation unnecessary. On the contrary, the more specialized the distribution format, the more important it becomes how you preserve the corresponding symbols and source.

Decide on policies like these:

Distribute the PDB as a separate file?
Use DebugType=embedded?
Keep symbols internal-only?
How will crash dumps be analyzed?

When using single-file publishing, trimming, AOT, and the like, the investigation experience can differ from ordinary IL assemblies. Before release, it is safest to run through “if it crashes, how do we read it?” at least once.

35. PDBs with Trimming / AOT

On current .NET, you may use trimming or Native AOT.

In that case, not just PDBs but also the generated native symbols and per-platform debug information come into play.

For example, you also need to think about native-side debug information: DWARF on Linux, dSYM on macOS, PDB on Windows.

Even for .NET applications, symbol design gets complex in configurations like these:

Native AOT
Self-contained publish
PublishSingleFile
ReadyToRun
Calling native DLLs via P/Invoke
Including C++/CLI

For ordinary web apps and class libraries, understanding Portable PDBs and Source Link is enough to start. But the more sophisticated the distribution format, the more “how do we analyze this crash?” needs to be part of the build design.

36. Notes for .NET Framework Projects

Old .NET Framework projects can differ from SDK-style projects in settings and defaults.

For example, differences like these:

The csproj format is old
packages.config is in use
The DebugType default differs from current .NET
Windows PDBs are in use
Source Link requires extra packages or MSBuild configuration
The CI's MSBuild version is old

The PDB principles are the same on .NET Framework:

Not required for execution
Important for debugging and failure investigation
The PDB must match the target binary
Release build PDBs should be retained

But copying current .NET instructions verbatim may not behave as expected in old projects.

With existing assets, check the actual output first:

msbuild MyApp.csproj /p:Configuration=Release
Get-ChildItem bin\Release -Filter *.pdb -Recurse

Then put the build settings and CI artifacts in order.

37. What About OSS Libraries?

For an OSS .NET library, we generally recommend this configuration:

Generate PDBs even in Release
Use Portable PDBs
Enable Source Link
Publish .snupkg to NuGet
Set the Repository metadata
Aim for deterministic builds

An example:

<PropertyGroup>
  <TargetFramework>net8.0</TargetFramework>
  <DebugType>portable</DebugType>
  <PublishRepositoryUrl>true</PublishRepositoryUrl>
  <IncludeSymbols>true</IncludeSymbols>
  <SymbolPackageFormat>snupkg</SymbolPackageFormat>
  <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>

Depending on the SDK and hosting provider, the extra Source Link package may be unnecessary. For older SDKs or unusual hosting, add the corresponding Microsoft.SourceLink.* package.

In OSS, the ability for consumers to step into your library is itself a quality attribute. “A library you can read when something goes wrong” earns trust on that basis alone.

38. What About Internal Libraries?

Source Link and PDBs are valuable for internal libraries too.

If anything, internal libraries are exactly where being able to step in from business apps helps most.

If you use an internal NuGet feed, consider these points:

Does the internal feed support .snupkg?
If not, do you include PDBs in the .nupkg?
Do you set up an internal symbol server?
How is authentication to the Git repository handled?
Can the source still be accessed after departures and transfers?

Just because it is internal, do not let PDBs exist only on someone’s local PC.

Generate them in CI and store them where the team can retrieve them.

39. What About On-Premises Products?

For on-premises products distributed into customer environments, PDB handling gets harder.

Bundling PDBs makes dumps and logs captured at customer sites easier to read. But it also makes the internal structure more visible.

Common options include:

Don't bundle PDBs, but retain the full versions on the vendor side
Provide only public symbols for customer support
Collect dumps on incidents and analyze them vendor-side using the PDBs
Provide limited symbol packages to critical customers

What matters is not losing PDBs after release.

With on-premises products, you sometimes investigate incidents in versions released years ago. If the corresponding PDB is gone at that point, investigation capability drops sharply.

40. What Happens If You Delete the PDBs?

If you delete the PDBs, the application still runs. But you will suffer later.

You might think rebuilding from the same commit can reproduce them. But perfect reproduction is surprisingly hard:

The SDK version differs
NuGet resolution results differ
Build time or environment variables differ
Generated code differs
Conditional compilation differs
There are settings applied only in CI
Dependent native tools differ

Deterministic builds improve reproducibility, but even so, “save them as artifacts from the start” is the safer policy.

PDBs are close to insurance. Their value emerges only when you need them. And if they are missing when you need them, there is no recovering.

There is no single configuration that fits every project. But for typical .NET applications, you can use these as the baseline.

Internal Applications

<PropertyGroup>
  <DebugType>portable</DebugType>
  <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>

The policy:

Generate PDBs even in Release
Decide production placement by operational policy
Always retain them in CI artifacts
Prepare a dump analysis procedure

Public NuGet Libraries

<PropertyGroup>
  <DebugType>portable</DebugType>
  <PublishRepositoryUrl>true</PublishRepositoryUrl>
  <IncludeSymbols>true</IncludeSymbols>
  <SymbolPackageFormat>snupkg</SymbolPackageFormat>
  <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>

The policy:

Enable Source Link
Publish .snupkg
Avoid unnecessary source embedding
Review the metadata being published

Small Internal Tools

<PropertyGroup>
  <DebugType>embedded</DebugType>
</PropertyGroup>

The policy:

Avoid forgotten PDBs
Accept the increase in distribution size
Restrict to internal use

Externally Distributed Products

Retain the full PDBs internally
Produce public symbols separately if needed
Review the contents of any PDBs included in customer deliverables
Define the dump collection and analysis procedure for support

42. A Checklist for Working with PDBs

Finally, here is a checklist for when you are unsure how to handle PDBs.

Which DLL / EXE does this PDB correspond to?
Which commit was that DLL / EXE built from?
Is it the PDB for the production Release build, not the Debug build?
Are the PDBs retained as CI artifacts?
Is there a symbol server or a retrieval procedure?
Is Source Link enabled?
Are the permissions on the source retrieval target appropriate?
Does the PDB contain anything you don't want disclosed?
For external distribution, is there a public/private symbol policy?
Have you verified that the PDB loads during dump analysis?

The three most important points:

A PDB is investigative information, not an executable
A PDB must match the target binary
PDBs must be retained before the production incident happens

43. Summary

The PDB is not the “mystery file” that appears next to your .dll or .exe — it is the debug information that connects the built binary to the source code developers can read.

It is not required for normal application execution. But it is critical for debugging, exception investigation, dump analysis, profiling, and stepping into external libraries.

PDBs are useful for Release builds too. In fact, what production incidents demand is the PDB corresponding to the Release build.

Whether to place PDBs in production environments is decided by your disclosure scope and operational policy. But generating PDBs and retaining them as build artifacts is necessary in almost every project.

In practice, make this the baseline policy:

Generate PDBs even in Release
Retain PDBs as CI/CD artifacts
Tie together the binary, PDB, commit ID, and build number
Make source reachable via Source Link
Consider .snupkg for NuGet libraries
Review the symbol information disclosed in external distribution

PDBs draw no attention while nothing is wrong. But when something goes wrong, they are the crucial clue that leads the investigator back to the source code.

Rather than “PDBs can be deleted and things still run,” think of it this way:

The PDB is the map your future failure investigation will need.

References

Recent articles sharing the same tags. Deepen your understanding with closely related topics.

These topic pages place the article in a broader service and decision context.

This article connects naturally to the following service pages.

Author Profile

Profile page for the article author.

Go Komura

Representative of KomuraSoft LLC

Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.

Back to the Blog