Using your own CEF-instance in Standalone Play mode.

I ran into a CEF initialization issue while integrating a bundled/custom CEF runtime into an Unreal Engine plugin.

The confusing part: everything worked in PIE / editor play, but failed in Play Standalone.

Symptoms

In Standalone, CEF failed before any rendering or page loading happened:

CefInitialize result: false
CefBrowserHost::CreateBrowserSync result=null
CreateBrowserSync returned null — CEF may not be initialized or the subprocess is missing.

This happened even though the obvious paths were valid:

browser_subprocess_path exists=true
resources_dir_path exists=true
locales_dir_path exists=true
libcef.dll exists=true

So the issue was not the render handler, texture upload, URL, JavaScript bridge, or UMG/material setup. The browser was never created.

Root cause

The problem was a shared CEF cache/profile path.

The Unreal Editor process and the Standalone game process were both using the same CEF profile/cache directory. CEF uses a process/profile singleton internally, so the second process can get redirected/refused by the already-running profile owner.

In practice, both processes were using something like:

.../SWUI_BundledCEF/Default

CEF logs may show clues such as:

Please customize CefSettings.root_cache_path for your application.
Handling STARTUP request from another process
Opening in existing browser session

The result:

shared CEF cache/profile
→ CEF process singleton conflict
→ CefInitialize returns false
→ CreateBrowserSync returns null
→ white screen / no browser

Fix

Give each Unreal process its own CEF root/cache/log path. Using the process ID is a simple and reliable approach.

#include "HAL/FileManager.h"
#include "HAL/PlatformProcess.h"
#include "Misc/Paths.h"

static void SetCefString(cef_string_t& Target, const FString& Value)
{
	const FTCHARToUTF8 Utf8Value(*Value);
	CefString(&Target).FromString(Utf8Value.Get(), Utf8Value.Length());
}

Then configure CEF like this before calling CefInitialize:

const uint32 ProcessId = FPlatformProcess::GetCurrentProcessId();

const FString RootCachePath = FPaths::ConvertRelativePathToFull(
	FPaths::Combine(
		FPlatformProcess::UserTempDir(),
		FString::Printf(TEXT("MyPlugin_CEF_%u"), ProcessId)
	)
);

const FString CachePath = FPaths::Combine(RootCachePath, TEXT("Default"));

const FString CefLogPath = FPaths::ConvertRelativePathToFull(
	FPaths::Combine(
		FPaths::ProjectLogDir(),
		FString::Printf(TEXT("MyPlugin_CEF_%u.log"), ProcessId)
	)
);

IFileManager::Get().MakeDirectory(*RootCachePath, true);
IFileManager::Get().MakeDirectory(*CachePath, true);

SetCefString(CefSettings.root_cache_path, RootCachePath);
SetCefString(CefSettings.cache_path, CachePath);
SetCefString(CefSettings.log_file, CefLogPath);

Also make sure CEF subprocess command-line arguments are not disabled:

CefSettings.command_line_args_disabled = 0;

CEF subprocesses need arguments such as:

--type=renderer
--type=gpu-process

Result

Instead of multiple Unreal processes sharing one CEF profile:

.../MyPlugin_CEF/Default

each process gets its own:

.../MyPlugin_CEF_8872/Default
.../MyPlugin_CEF_14868/Default

After this change, Standalone initialized correctly:

CefInitialize result: true
CreateBrowserSync result=valid

Debugging tip

If you are debugging CEF inside Unreal, first check whether CefInitialize actually succeeds. If it returns false, do not waste time debugging rendering, texture upload, URLs, or JavaScript integration yet.

The browser does not exist until initialization succeeds.

On Windows, you can also check which CEF DLLs are loaded into the Unreal process:

Get-Process UnrealEditor | ForEach-Object {
  $p=$_.Id
  Get-Process -Id $p -Module -ErrorAction SilentlyContinue |
    Where-Object { $_.ModuleName -match "libcef|chrome_elf" } |
    Select-Object @{n="PID";e={$p}},ModuleName,FileName
}

This is especially useful if your plugin ships its own CEF while Unreal’s own WebBrowser/Fab/editor features may also load a CEF runtime.