What Is a PDB (Program Database)? — Understanding Debug Information, Symbols, and Source Link
· Go Komura · .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
.pdbfiles in production? - Does the application fail to run without the
.pdb? - Is it wrong that a Release build produces a
.pdb? - Does the
.pdbcontain the entire source code? - Does having a
.pdbguarantee you can set breakpoints? - Why is a
.pdbneeded for dump analysis and failure investigation? - How should
.pdbfiles 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.
20. What Is Source Link?
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.
21. Source Link in .NET 8 and Later
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.
41. Recommended Settings in Practice
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
- Symbols in .NET - Microsoft Learn
- Specify symbol (.pdb) and source files in the Visual Studio debugger - Microsoft Learn
- C# Compiler Options that control code generation - Microsoft Learn
- Portable PDB Symbols - Microsoft Learn
- Source Link included in the .NET SDK - Microsoft Learn
- dotnet/sourcelink - GitHub
- Creating symbol packages (.snupkg) - Microsoft Learn
- Public and Private Symbols - Microsoft Learn
- Using PDBCopy - Microsoft Learn
- dotnet-symbol diagnostic tool - Microsoft Learn
- dotnet-dump diagnostic tool - Microsoft Learn
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
Telling GC Lag from a Memory Leak in .NET — A Practical Procedure for Observing, Comparing, and Proving Memory Growth
A procedure for determining whether growing memory in a .NET application is simply waiting on garbage collection or a genuine memory leak...
Handling Windows Impersonation Tokens Correctly — Borrowing Privileges per Thread and Reverting Safely
A practical guide to Windows impersonation tokens — access tokens, primary tokens, thread tokens, impersonation levels, RevertToSelf, and...
The Misconception That TCP Lets You Receive in the Same Units You Send — Designing Reception Around a Byte Stream
Assuming TCP delivers data in the same units as Send or Write leads to fragmentation, coalescing, garbled text, and broken protocols. Thi...
What Is Roslyn? Reading, Fixing, and Generating C# Code from the Compiler's Point of View
An overview of Roslyn (the .NET Compiler Platform): Syntax Trees, SemanticModel, Workspaces, Analyzers, Source Generators, and where they...
Windows App Outsourcing and Contract Development: What to Sort Out Before You Ask
Before commissioning Windows app outsourcing or contract development, here is how to sort out existing software modification, device inte...
Related Topics
These topic pages place the article in a broader service and decision context.
Windows Technical Topics
Topic hub for KomuraSoft LLC's Windows development, investigation, and legacy-asset articles.
Bug Investigation & Long-Run Failures
Topic page for intermittent failures, communication diagnosis, long-run crashes, and failure-path test foundations.
Where This Topic Connects
This article connects naturally to the following service pages.
Windows App Development
We support Windows desktop applications that involve resident processing, device integration, operational logging, and maintainable structure.
Bug Investigation & Root Cause Analysis
We investigate difficult production issues such as intermittent failures, long-run crashes, leaks, and communication stoppages.
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.
Public links