Azure Functions LangGraph
⚠️ Experimental — pattern exploration. APIs and behavior may change. Not recommended as a production dependency yet.
Part of the Azure Functions Python DX Toolkit — dogfood-tested by azure-functions-cookbook-python.
Read this in: 한국어 | 日本語 | 简体中文
Stability — rolling 0.x, per surface. This package follows a rolling 0.x development model rather than a single "everything breaks until v1.0" contract. Core adapter APIs aim for backward compatibility across minor releases, while newer surfaces may still evolve. The per-surface stability table below is the single source of truth for what you can rely on; the
Development Status :: 3 - Alphaclassifier inpyproject.tomlreflects the youngest surfaces, not the stable core. Please report issues on GitHub.
Surface Tier What it means Core — LangGraphAppconstruction, native invoke / stream / state HTTP endpoints, auth levelsStable API frozen; a breaking change warrants a major bump Checkpoint backends, thread locking, native async runtime ( ainvoke/astream),version="v2"pass-through,RunObservercontractBeta Shape settled; may be refined before v1.0 LangGraph Platform compatibility, true HTTP streaming, Azure Service Bus trigger, Durable async run lifecycle Experimental May change or be removed between minor releases
Deploy LangGraph graphs as Azure Functions HTTP endpoints with minimal boilerplate.
Part of the Azure Functions Python DX Toolkit
Python 3.10 is deprecated. Support ends in the next minor release — Python 3.10 reaches end of life in October 2026. Importing the package on Python 3.10 emits a
FutureWarning; upgrade to Python 3.11 or newer.
Why this exists
Deploying LangGraph on Azure Functions is harder than it should be.
- LangGraph does not provide an Azure Functions-native deployment adapter
- Exposing compiled graphs as HTTP endpoints requires repetitive wiring
- Teams often rebuild the same invoke/stream wrapper for every project
This package provides a focused adapter for serving LangGraph graphs on Azure Functions Python v2.
What it does
- Zero-boilerplate deployment — register a compiled graph, get HTTP endpoints automatically
- Invoke endpoint —
POST /api/graphs/{name}/invokefor synchronous execution - Stream endpoint —
POST /api/graphs/{name}/streamfor buffered SSE responses (opt into true incremental streaming withStreamingLangGraphApp) - Health endpoints — anonymous
GET /api/healthliveness probe ({"status": "ok"}, no graph inventory) plus protectedGET /api/health/detailslisting registered graphs with checkpointer status - Checkpointer pass-through — thread-based conversation state works via LangGraph's native config
- State endpoint —
GET /api/graphs/{name}/threads/{thread_id}/statefor thread state inspection (when supported) - Per-graph auth — override app-level auth with
register(..., auth_level=...) - LangGraph Platform API compatibility — SDK-compatible endpoints for threads, runs, assistants, and state (v0.3+)
- Persistent storage backends — Azure Blob Storage checkpointer and Azure Table Storage thread store (v0.4+)
LangGraph Platform comparison
| Feature | LangGraph Platform | azure-functions-langgraph |
|---|---|---|
| Hosting | LangChain Cloud (paid) | Your Azure subscription |
| Assistants | Built-in | SDK-compatible API (v0.3+) |
| Thread lifecycle | Built-in | Create, get, update, delete, search, count (v0.3+) |
| Runs | Built-in | Threaded + threadless runs (v0.4+) |
| State read/update | Built-in | get_state + update_state (v0.4+) |
| State history | Built-in | Checkpoint history with filtering (v0.4+) |
| Streaming | True SSE | Buffered SSE (opt-in true SSE via StreamingLangGraphApp) |
| Persistent storage | Built-in | Azure Blob + Table Storage (v0.4+) |
| Infrastructure | Managed | Azure Functions (serverless) |
| Cost model | Per-seat/usage | Azure Functions pricing |
See COMPATIBILITY.md for the per-feature SDK support matrix, including which
RunCreatefields, thread filters, and SDK calls return501 Not Implemented.
Scope
- Azure Functions Python v2 programming model
- LangGraph graph deployment and HTTP exposure
- LangGraph runtime concerns: invoke, stream, threads, runs, and state
- Optional integration points for validation and OpenAPI via companion packages
This package is a deployment adapter — it wraps LangGraph, it does not replace it.
Internally, graph registration remains protocol-based (
LangGraphLike), so any object satisfying the protocol works — but the package's documentation and examples focus on LangGraph use cases.
What this package does not do
This package does not own:
- OpenAPI document generation or Swagger UI — use
azure-functions-openapi-python - Request/response validation beyond LangGraph contracts — use
azure-functions-validation-python - Generic graph-serving abstractions beyond LangGraph
Note: For OpenAPI spec generation, use the
azure-functions-openapi-pythonpackage with the bridge module (azure_functions_langgraph.openapi.register_with_openapi).
Installation
pip install azure-functions-langgraph
For persistent storage with Azure services:
# Azure Blob Storage checkpointer
pip install azure-functions-langgraph[azure-blob]
# Azure Table Storage thread store
pip install azure-functions-langgraph[azure-table]
# Both
pip install azure-functions-langgraph[azure-blob,azure-table]
For database or Cosmos DB checkpointer backends:
# Postgres checkpointer
pip install azure-functions-langgraph[postgres]
# SQLite checkpointer (local dev)
pip install azure-functions-langgraph[sqlite]
# Cosmos DB checkpointer
pip install azure-functions-langgraph[cosmos]
Your Azure Functions app should also include:
azure-functions
langgraph
azure-functions-langgraph
For local development:
git clone https://github.com/yeongseon/azure-functions-langgraph-python.git
cd azure-functions-langgraph
pip install -e .[dev]
Quick Start
New to this package? Follow the Deploy a LangGraph agent to Azure Functions in 5 minutes tutorial — a genuinely runnable, CI-smoke-tested walkthrough from a compiled graph to live HTTP endpoints, locally and on Azure.
Recommended learning path
Once the Quick Start echo agent runs, follow these four examples in order. Each one is the minimal delta over the previous step, so you add exactly one production concern at a time — real LLM → memory → tools → durable persistence:
| Step | Example | What it adds |
|---|---|---|
| 1 | azure_openai_agent |
A real agent, not an echo — a deployable LangGraph agent backed by a real Azure OpenAI deployment, with API-key and Managed Identity paths. |
| 2 | conversation_memory |
Memory — the same agent plus a checkpointer, so the same thread_id keeps a conversation going across requests. |
| 3 | tool_calling_agent |
Tools — the same agent plus a ToolNode loop, with the tools as your own code in tools.py and an OpenAPI/Swagger bridge wired in. |
| 4 | production_persistent_agent |
Durable production persistence — the same real agent whose per-thread_id memory is made durable on an Azure Blob checkpointer, wired with Managed Identity in production and Azurite locally. |
This azure_openai_agent → conversation_memory → tool_calling_agent → production_persistent_agent progression takes you from a real agent to a durable, production-ready one end to end. Every example is a standalone Azure Functions app with a deterministic fake-model fallback, so all four pass CI without any cloud credentials.
from langgraph.graph import END, START, StateGraph
from typing_extensions import TypedDict
import azure.functions as func
from azure_functions_langgraph import LangGraphApp
# 1. Define your state
class AgentState(TypedDict):
messages: list[dict[str, str]]
# 2. Define your nodes
def chat(state: AgentState) -> dict:
user_msg = state["messages"][-1]["content"]
return {"messages": state["messages"] + [{"role": "assistant", "content": f"Echo: {user_msg}"}]}
# 3. Build graph
builder = StateGraph(AgentState)
builder.add_node("chat", chat)
builder.add_edge(START, "chat")
builder.add_edge("chat", END)
graph = builder.compile()
# 4. Deploy (default is `AuthLevel.FUNCTION` — requires a function key when deployed).
app = LangGraphApp()
app.register(graph=graph, name="echo_agent")
func_app = app.function_app # ← use this as your Azure Functions app
Start the Functions host locally:
func start
Verify locally and on Azure
After deploying (see docs/deployment.md), the same request produces the same response in both environments. Azure requires a function key (?code=<FUNCTION_KEY>) when auth_level is set to FUNCTION.
Local
curl -s http://localhost:7071/api/health
{"status": "ok"}
The anonymous liveness probe returns only {"status": "ok"}. To see the
registered-graph inventory, call the protected details endpoint:
curl -s http://localhost:7071/api/health/details
{"status": "ok", "graphs": [{"name": "echo_agent", "description": null, "has_checkpointer": false}]}
Azure
# Liveness probe (anonymous by default)
curl -s "https://<your-app>.azurewebsites.net/api/health"
{"status": "ok"}
# Detailed inventory (protected — defaults to the app auth_level)
curl -s "https://<your-app>.azurewebsites.net/api/health/details?code=<FUNCTION_KEY>"
{"status": "ok", "graphs": [{"name": "echo_agent", "description": null, "has_checkpointer": false}]}
Response format verified against a temporary Azure Functions deployment of the
simple_agentexample in koreacentral (Python 3.12, Consumption plan). The Quick Start usesecho_agentfor illustration; the health endpoint returns the same JSON structure regardless of graph name. URL anonymized.
Production authentication
Important:
LangGraphAppdefaults toAuthLevel.FUNCTION, so deployed endpoints require a function key (?code=<FUNCTION_KEY>or thex-functions-keyheader) out of the box. For an unauthenticated public surface — e.g. local development againstfunc startwhen you also want to hit the endpoint without a key — passauth_level=func.AuthLevel.ANONYMOUSexplicitly. Doing so emits an unconditionalUserWarningso an accidental anonymous deployment is loud in test and CI output:
import azure.functions as func
from azure_functions_langgraph import LangGraphApp
# Production (default): require function key authentication
app = LangGraphApp() # equivalent to LangGraphApp(auth_level=func.AuthLevel.FUNCTION)
# Local dev only: explicit opt-in to anonymous — emits a UserWarning
app_local = LangGraphApp(auth_level=func.AuthLevel.ANONYMOUS)
Note: There are two health surfaces. The liveness probe
GET /api/healthuseshealth_auth_level, which defaults toANONYMOUSindependently ofauth_level, and returns only{"status": "ok"}— it never enumerates registered graphs. Sethealth_auth_level=func.AuthLevel.FUNCTIONto require a key on the probe as well.The detailed inventory
GET /api/health/details(graph names, descriptions, and checkpointer status) useshealth_details_auth_level, which defaults to the app-levelauth_level(FUNCTION) — so the inventory is protected by default. Passhealth_details_auth_level=func.AuthLevel.ANONYMOUSexplicitly to expose it publicly (e.g. local development).
Streaming behavior
Default (
LangGraphApp): buffered SSE. All/streamendpoints on the classicLangGraphApp(the nativePOST /api/graphs/{name}/streamand the Platform-compatiblePOST /threads/{id}/runs/streamandPOST /runs/stream) return buffered SSE. Chunks emitted by the graph are collected during execution and flushed as SSE events after the run completes — this is not true token-level streaming, and clients will not receive partial tokens incrementally. Buffered SSE is the classic adapter's deliberate implementation choice, because the classicHttpRequest/HttpResponsemodel it is built on cannot flush a response incrementally.Opt-in true streaming:
StreamingLangGraphApp(experimental). For genuine incremental delivery — eachevent: dataframe flushed as the graph produces it — use the separateStreamingLangGraphApp. It serves the graph over the FastAPI/ASGI transport provided by theazurefunctions-extensions-http-fastapiextension (Azure Functions runtime 4.34.1+), gated behind thestreamingextra:pip install "azure-functions-langgraph[streaming]"from azure_functions_langgraph import StreamingLangGraphApp app = StreamingLangGraphApp() app.register(graph=graph, name="my_agent") func_app = app.function_appEnabling true streaming converts the entire function app to the ASGI model, so it cannot be mixed with classic
LangGraphApproutes in the same app — deploy a separate app if you also need the classic buffered surface, and note the classicmax_stream_response_bytesbyte guard is replaced bymax_stream_events(a frame count cap) because a true stream is never buffered.LangGraphAppis unchanged and remains the default. Seeexamples/true_streaming_agent/for a runnablecurl -Nwalkthrough that proves incremental arrival.
Run observability
Wire a RunObserver to receive
run-lifecycle signals (started / completed / failed / rejected) for every
native invoke/stream run and every Platform runs/wait / runs/stream run. Each
callback receives an immutable RunContext of correlation identifiers and
timing only — never input, output, config, headers, or secrets.
The package ships a built-in, dependency-free LoggingRunObserver for
zero-boilerplate telemetry:
import logging
from azure_functions_langgraph import LangGraphApp, LoggingRunObserver
logging.getLogger("azure_functions_langgraph.observability.run").setLevel(logging.INFO)
app = LangGraphApp(observer=LoggingRunObserver())
app.register(graph=graph, name="my_agent")
It emits one structured log record per lifecycle event under a single extra
key (langgraph_run) — graph_name, endpoint, run_id, thread_id,
assistant_id, stream_mode, transport, has_checkpointer, lock_backend,
duration_ms, a derived status, and (on failure) the exception error_type
(class name only). Azure Functions' Application Insights integration surfaces
these under customDimensions.
The package owns the domain signal only — not log formatting, App Insights ingestion, OpenTelemetry exporters, sampling, or PII redaction. Observer failures are isolated and never fail a graph run. To write your own observer, see
examples/run_observer/; for an App Insights + KQL walkthrough, seeexamples/observability_app_insights/.
OpenTelemetry observer (optional otel extra)
For distributed-tracing backends, the package ships an optional
OTelRunObserver that maps each run's lifecycle to a single OpenTelemetry
span. It is gated behind the otel extra:
pip install azure-functions-langgraph[otel]
from azure_functions_langgraph import LangGraphApp, OTelRunObserver
# Uses whatever TracerProvider the operator has already installed.
app = LangGraphApp(observer=OTelRunObserver())
app.register(graph=graph, name="my_agent")
on_run_started opens a span (keyed by run_id); the terminal event
(completed / failed / rejected) closes it with the matching
StatusCode, a langgraph.status, and a langgraph.duration_ms. Span
attributes come only from the safe RunContext correlation fields — never
input, output, config, headers, or secrets — and failures record the exception
type only.
Consistent with the observability boundary above,
OTelRunObserverdoes not configure aTracerProvider, exporters, sampling, or resource attributes — the operator owns all SDK/exporter wiring. The observer only acquires a tracer viaopentelemetry.trace.get_tracer(...)and emits spans onto whatever provider is installed. For a runnable local demo with a console exporter, seeexamples/run_observer_otel/.
Service Bus trigger (experimental)
Beyond the HTTP surface, a compiled graph can be driven directly from an
Azure Service Bus message via register_service_bus. Each delivered
message is mapped to a single invoke (or ainvoke) call — useful for
background jobs and decoupled pipelines where work arrives as messages rather
than synchronous HTTP requests. A graph may be registered on both surfaces.
from azure_functions_langgraph import LangGraphApp
app = LangGraphApp()
# Queue trigger
app.register_service_bus(
graph=graph,
name="jobs_agent",
connection="ServiceBusConnection", # app-setting name
queue_name="langgraph-jobs",
)
# Or a topic subscription trigger
app.register_service_bus(
graph=graph,
name="events_agent",
connection="ServiceBusConnection",
topic_name="events",
subscription_name="langgraph",
)
func_app = app.function_app
HTTP vs Service Bus
HTTP (register) |
Service Bus (register_service_bus) |
|
|---|---|---|
| Invocation | Synchronous POST /api/graphs/{name}/invoke |
One graph run per queue/topic message |
| Caller gets result | Yes — in the HTTP response | No — use result_handler to route output |
| Delivery | At-most-once (caller retries) | At-least-once (binding abandons/retries) |
| Backpressure | Caller-driven | Queue buffers + Functions scale-out |
| Best for | Interactive request/response | Background jobs, decoupled pipelines |
| Errors | Returned to caller | Not swallowed → message abandoned/retried |
Message mapping. By default the message body is decoded as UTF-8 and parsed
as JSON: a JSON object is passed through as the graph input verbatim; any other
value (or non-JSON text) is wrapped as
{"messages": [{"role": "human", "content": <text>}]}. Pass input_mapper=...
to customize.
Thread state and locking. Runs are threadless by default. Pass
thread_id_factory=... to derive a checkpoint thread_id from a message; when
it returns a value and the graph has a checkpointer, the app-level
thread_lock guards the run so concurrent deliveries for the same logical
thread do not mutate checkpoints concurrently. If the lock cannot be acquired a
ThreadContentionError is raised so the binding abandons and retries.
Error handling and telemetry. Graph exceptions are not swallowed — a
failing run propagates so the message is retried / dead-lettered per your
queue's policy. This package ships no Service Bus output binding; pass a
result_handler(result, msg) to publish results. Message bodies and
application-property values are never included in telemetry; runs carry a
trigger_type="service_bus" correlation field so they are distinguishable from
HTTP runs in your observer. See
examples/service_bus_agent/ for a full wiring.
Durable async run lifecycle (experimental)
For long-running graphs, opt into an experimental Durable Functions-backed
async run control plane with LangGraphApp(async_runs="durable"). This adds a
create / poll / cancel HTTP surface on top of the classic invoke/stream routes:
from azure_functions_langgraph import LangGraphApp
app = LangGraphApp(async_runs="durable") # requires the `durable` extra
app.register(graph=graph, name="my_agent")
func_app = app.function_app
pip install "azure-functions-langgraph[durable]"
| Route | Purpose |
|---|---|
POST /api/graphs/{name}/runs |
Start a run — returns 202 with a run_id and pending status |
GET /api/runs/{run_id} |
Poll normalized run status (pending / running / completed / failed / canceled) |
POST /api/runs/{run_id}/cancel |
Terminate an in-flight run |
The graph executes inside a Durable activity, never the orchestrator, so
Durable replay determinism is preserved — no LLM, tool, network, clock, random,
or graph user code runs in the orchestrator. Durable history is not a
replacement for LangGraph checkpoints; reuse a checkpointer for conversation
state as usual. Durable dependencies stay optional behind the durable extra.
See examples/durable_async_agent/ for a full
create → poll → result → cancel walkthrough.
Per-graph auth
Override app-level auth settings per graph:
# Per-graph authentication override
app.register(graph=public_graph, name="public", auth_level=func.AuthLevel.ANONYMOUS)
app.register(graph=private_graph, name="private", auth_level=func.AuthLevel.FUNCTION)
Example request using a function key:
curl -X POST "https://<app>.azurewebsites.net/api/graphs/echo_agent/invoke?code=<FUNCTION_KEY>" \
-H "Content-Type: application/json" \
-d '{"input": {"messages": [{"role": "human", "content": "Hello!"}]}}'
Custom route prefix
All routes use the default /api prefix set by Azure Functions. To change it,
configure routePrefix in your host.json:
{
"extensions": {
"http": {
"routePrefix": "v1"
}
}
}
This changes all routes (e.g. POST /v1/graphs/{name}/invoke). Set routePrefix
to "" to remove the prefix entirely.
Important —
LangGraphApp(route_prefix=...)is metadata-only. Azure Functions resolves HTTP routes fromhost.json(the source of truth), not from the constructor argument. Theroute_prefixargument is recorded into the metadata snapshot consumed by tooling such as theazure-functions-openapi-pythonbridge so generated specs reflect the deployed routes, but changing it without also updatinghost.jsondoes not change where requests are served. Always edithost.jsonto actually move routes, and pass the same value toLangGraphApp(route_prefix=...)so metadata stays in sync.
What you get
POST /api/graphs/echo_agent/invoke— invoke the agentPOST /api/graphs/echo_agent/stream— stream agent responses (buffered SSE; opt into true incremental streaming withStreamingLangGraphApp)GET /api/graphs/echo_agent/threads/{thread_id}/state— inspect thread stateGET /api/health— liveness probe ({"status": "ok"})GET /api/health/details— registered-graph inventory (protected by default)
With platform_compat=True, you also get SDK-compatible endpoints:
POST /assistants/search— list registered assistantsGET /assistants/{id}— get assistant detailsPOST /assistants/count— count assistantsPOST /threads— create threadGET /threads/{id}— get threadPATCH /threads/{id}— update thread metadataDELETE /threads/{id}— delete threadPOST /threads/search— search threadsPOST /threads/count— count threadsPOST /threads/{id}/runs/wait— run and wait for resultPOST /threads/{id}/runs/stream— run and stream result (buffered SSE)POST /runs/wait— threadless runPOST /runs/stream— threadless stream (buffered SSE)GET /threads/{id}/state— get thread statePOST /threads/{id}/state— update thread statePOST /threads/{id}/history— get state history
Request format
{
"input": {
"messages": [{"role": "human", "content": "Hello!"}]
},
"config": {
"configurable": {"thread_id": "conversation-1"}
}
}
Persistent storage (v0.4+)
Use Azure Blob Storage for checkpoint persistence and Azure Table Storage for thread metadata:
import azure.functions as func
from azure.storage.blob import ContainerClient
from langgraph.graph import END, START, StateGraph
from typing_extensions import TypedDict
from azure_functions_langgraph import LangGraphApp
from azure_functions_langgraph.checkpointers.azure_blob import AzureBlobCheckpointSaver
from azure_functions_langgraph.stores.azure_table import AzureTableThreadStore
class AgentState(TypedDict):
messages: list[dict[str, str]]
def chat(state: AgentState) -> dict:
user_msg = state["messages"][-1]["content"]
return {"messages": state["messages"] + [{"role": "assistant", "content": f"Echo: {user_msg}"}]}
# Build graph with Azure Blob checkpointer
container_client = ContainerClient.from_connection_string(
"DefaultEndpointsProtocol=https;AccountName=...", "checkpoints"
)
saver = AzureBlobCheckpointSaver(container_client=container_client)
builder = StateGraph(AgentState)
builder.add_node("chat", chat)
builder.add_edge(START, "chat")
builder.add_edge("chat", END)
graph = builder.compile(checkpointer=saver)
# Deploy with Azure Table thread store
thread_store = AzureTableThreadStore.from_connection_string(
"DefaultEndpointsProtocol=https;AccountName=...", table_name="threads"
)
# Production: always set auth_level explicitly
app = LangGraphApp(platform_compat=True, auth_level=func.AuthLevel.FUNCTION)
app.thread_store = thread_store
app.register(graph=graph, name="echo_agent")
func_app = app.function_app
Checkpoints and thread metadata survive Azure Functions restarts and scale across instances.
Persistent storage with Managed Identity
The recommended production wiring uses Managed Identity instead of connection strings, so no secrets land in App Settings. Install the azure-identity extra and pass DefaultAzureCredential to both clients:
pip install azure-functions-langgraph[azure-blob,azure-table,azure-identity]
from azure.data.tables import TableClient
from azure.identity import DefaultAzureCredential
from azure.storage.blob import ContainerClient
from azure_functions_langgraph.checkpointers.azure_blob import AzureBlobCheckpointSaver
from azure_functions_langgraph.stores.azure_table import AzureTableThreadStore
credential = DefaultAzureCredential()
container_client = ContainerClient(
account_url="https://<account>.blob.core.windows.net",
container_name="langgraph-checkpoints",
credential=credential,
)
table_client = TableClient(
endpoint="https://<account>.table.core.windows.net",
table_name="langgraphthreads",
credential=credential,
)
checkpointer = AzureBlobCheckpointSaver(container_client=container_client)
thread_store = AzureTableThreadStore.from_table_client(table_client=table_client)
Required role assignments on the storage account (or narrower scopes):
| Role | Used by |
|---|---|
Storage Blob Data Contributor |
AzureBlobCheckpointSaver |
Storage Table Data Contributor |
AzureTableThreadStore |
DefaultAzureCredential walks a chain of credentials. In Azure Functions it picks up the Function App's Managed Identity; locally it falls back to AzureCliCredential (az login) — the same code path works in both environments without conditional wiring.
For a complete runnable example (Managed Identity in prod, Azurite + connection string locally), see examples/managed_identity_storage/.
Native async checkpoint I/O
By default AzureBlobCheckpointSaver is configured with a synchronous
ContainerClient. Its async methods (aget_tuple, alist, aput,
aput_writes, adelete_thread) remain fully available: when a graph is driven
through the async endpoints (graph.ainvoke / graph.astream) without an aio
client, they run the synchronous implementation in a thread executor and emit a
one-time warning — correct, but not truly non-blocking.
To get native, non-blocking blob I/O on the async path, pass an
azure.storage.blob.aio.ContainerClient as aio_container_client alongside the
synchronous container_client. Both clients share the same container; the sync
methods use the sync client and the async methods use the aio client, so
checkpoints written by one path are readable by the other:
from azure.identity import DefaultAzureCredential
from azure.identity.aio import DefaultAzureCredential as AioDefaultAzureCredential
from azure.storage.blob import ContainerClient
from azure.storage.blob.aio import ContainerClient as AioContainerClient
container_client = ContainerClient(
account_url="https://<account>.blob.core.windows.net",
container_name="langgraph-checkpoints",
credential=DefaultAzureCredential(),
)
aio_container_client = AioContainerClient(
account_url="https://<account>.blob.core.windows.net",
container_name="langgraph-checkpoints",
credential=AioDefaultAzureCredential(),
)
checkpointer = AzureBlobCheckpointSaver(
container_client=container_client,
aio_container_client=aio_container_client,
)
The caller owns the lifecycle of both clients (open/close), exactly as with the synchronous client.
aio_container_clientis fully optional and backward-compatible — omitting it preserves the previous executor-fallback behaviour.
Checkpoint store security
The checkpointer backends persist graph state using LangGraph's default serializer, which can restore arbitrary Python types from a checkpoint payload. Treat the checkpoint store as part of your threat model — checkpoint blobs are not trusted-free data:
- Restrict storage access with RBAC / Managed Identity (above) and private endpoints so only the Function App identity can read or write checkpoints.
- Enable strict deserialization by setting
LANGGRAPH_STRICT_MSGPACK=true(plus any allowed modules your graphs need) in your Function App application settings. The env var is read at import time, so it must be set before the app imports LangGraph — configure it as an app setting, not at runtime. Upstream defaults to permissive (false).
See the upstream advisory
GHSA-g48c-2wqr-h844
and docs/security.md for the full
rationale.
Scale envelope
The bundled persistent backends are intended for development and small-to-medium production deployments. Plan ahead before pushing past these limits:
| Backend | Comfortable | Caution zone | Switch backends |
|---|---|---|---|
AzureBlobCheckpointSaver |
< 100 checkpoints/thread, < 10K threads | 100–1000 checkpoints/thread | Use Cosmos DB or Redis-backed checkpointer |
AzureTableThreadStore |
< 100K threads, light search load | 100K–500K threads | Use a sharded thread store or Cosmos DB |
Notes:
- Single partition —
AzureTableThreadStorekeys every thread under a single PartitionKey, capped by Azure Table per-partition throughput (~2000 entities/sec on Standard accounts). Search and count beyondstatusfiltering are client-side; see COMPATIBILITY.md. - Prefix scans —
AzureBlobCheckpointSaverlists checkpoints via blob prefix scans; transaction count and latency grow with checkpoints-per-thread. Use the retention helpers below to keep that bounded. - Entity size — Azure Table entities are capped at 1 MB; the store logs a warning at 90% of the threshold.
Retention helpers
AzureBlobCheckpointSaver exposes two helpers for scheduled cleanup (e.g. from a Timer-triggered Function):
# Keep only the most recent 50 checkpoints per (thread, namespace)
saver.delete_old_checkpoints(thread_id="conversation-1", keep_last=50)
# Or delete everything older than a known checkpoint id
saver.delete_checkpoints_before(
thread_id="conversation-1",
before_checkpoint_id="01HXY...",
)
Both helpers only delete checkpoint marker, metadata, and write blobs. They intentionally preserve channel value blobs (under values/) and the latest.json pointer so retained checkpoints remain fully usable.
Note —
delete_old_checkpoints/delete_checkpoints_beforeare safe but not exhaustive. Channel value blobs that were referenced only by the now-deleted checkpoints become orphaned and are not removed. For long-running threads with frequent checkpointing, those orphans can dominate the storage footprint over time. Runcollect_orphaned_values()(below) on a schedule as the second step.
Garbage-collecting orphaned channel values
After pruning checkpoints, collect_orphaned_values() walks the surviving checkpoints, builds the set of (channel, version) pairs they reference, and removes any values/ blob outside that set. Default is dry-run so you can audit first:
# Dry run — see what would be deleted, change nothing
audit = saver.collect_orphaned_values(thread_id="conversation-1")
print(audit.would_delete)
# Real run — actually delete orphans
result = saver.collect_orphaned_values(thread_id="conversation-1", dry_run=False)
print(f"Deleted {len(result.deleted)} orphaned value blobs")
The helper is concurrency-safe by two complementary mechanisms:
- Recent-write grace period — value blobs whose
last_modifiedis withingrace_period_seconds(default 300s) are deferred to a future GC pass and recorded inresult.skipped_recent. This protects the window between a value blob being uploaded and its checkpoint commit marker (latest.json) being finalized. - Per-orphan re-scan — immediately before each delete, the survivor set is recomputed; an older value blob that a newly finalized checkpoint started referencing after the snapshot is preserved (such blobs appear in
would_deletebut notdeleted).
Per namespace, the helper fails closed — if latest.json is missing or any surviving checkpoint blob is unreadable / fails deserialization, the namespace is skipped entirely (recorded in result.skipped_namespaces) so a misconfigured or transiently-unavailable store cannot trigger destructive deletion.
Set grace_period_seconds=0 to disable the recent-write guard during an offline maintenance window when no concurrent checkpoint writes are possible.
DB checkpointer backends
For workloads that already run a managed database (or need state shared across multiple Function instances), thin DX helpers wrap the official LangGraph DB checkpoint packages without reimplementing storage:
| Backend | Helper | Extra | When to use |
|---|---|---|---|
| Postgres | create_postgres_checkpointer |
pip install azure-functions-langgraph[postgres] |
Production, multi-instance, existing Postgres infra |
| SQLite | create_sqlite_checkpointer |
pip install azure-functions-langgraph[sqlite] |
Local dev and single-instance deployments |
| Cosmos DB | create_cosmos_checkpointer |
pip install azure-functions-langgraph[cosmos] |
Azure-native serverless/global production |
Each helper owns the connection lifetime and emits clear ImportErrors pointing at the right extra. The Postgres and SQLite helpers accept a connection string and (by default) call upstream setup() on cold start so the checkpoint tables exist; the Cosmos DB helper accepts an endpoint and key, temporarily wires the upstream COSMOSDB_ENDPOINT / COSMOSDB_KEY environment variables, and directly instantiates the upstream CosmosDBSaver:
Authentication —
create_cosmos_checkpointeris a key-based convenience wrapper; Managed Identity is available upstream. The DX helper resolves an account key and temporarily wiresCOSMOSDB_ENDPOINT/COSMOSDB_KEYbefore instantiating the upstreamCosmosDBSaver, so calling the helper always uses key-based auth. However, the upstreamlanggraph-checkpoint-cosmosdbpackage (≥ 0.2.8) does support passwordless auth:CosmosDBSaverfalls back toDefaultAzureCredential(Managed Identity,az login, service principal) wheneverCOSMOSDB_KEYis unset. To use Managed Identity today, skip the helper and instantiate the upstream saver directly with onlyCOSMOSDB_ENDPOINTset (no key):
import os
from langgraph_checkpoint_cosmosdb import CosmosDBSaver
os.environ["COSMOSDB_ENDPOINT"] = "https://<account>.documents.azure.com:443/"
# Leave COSMOSDB_KEY unset → upstream uses DefaultAzureCredential (Managed Identity)
checkpointer = CosmosDBSaver(database_name="langgraph", container_name="checkpoints")
Grant the Function App's identity a Cosmos DB data-plane role (e.g. Cosmos DB Built-in Data Contributor) on the account. The official
langchain-azure-cosmosdbpackage is an alternative that also supportsDefaultAzureCredential. A future release may extendcreate_cosmos_checkpointerwith a first-class Managed Identity path; until then the helper remains key-based by design.
import os
from azure_functions_langgraph.checkpointers.postgres import create_postgres_checkpointer
checkpointer = create_postgres_checkpointer(
os.environ["LANGGRAPH_POSTGRES_CONNECTION_STRING"],
setup=True, # set False once your deployment pipeline owns migrations
)
graph = builder.compile(checkpointer=checkpointer)
The helpers do not hide builder.compile(checkpointer=...) and do not reimplement DB checkpoint storage — they centralize connection conventions and emit clear ImportErrors pointing at the right extra. The Postgres and SQLite helpers run setup() once at cold start; the Cosmos DB helper directly instantiates the saver (no setup() call, no context manager). See examples/postgres_checkpoint_production/, examples/sqlite_checkpoint_local/, and examples/cosmos_checkpoint_azure/ for full Azure-Functions wiring.
| Backend | Comfortable | Caution zone | Switch backends |
|---|---|---|---|
create_sqlite_checkpointer |
local dev, single-instance prod | multi-process write contention | Use Postgres |
create_postgres_checkpointer |
multi-instance Functions, existing Postgres infra | very high write QPS without read replicas | Add connection pooling / read replicas, or shard |
create_cosmos_checkpointer |
Azure-native serverless, global distribution | high RU cost with large checkpoints | Tune RU allocation, use provisioned throughput |
Run lock semantics
When using AzureTableThreadStore with Platform-compatible runs, each graph execution acquires an atomic run lock (ETag compare-and-swap) on the thread before invoking the graph. On completion — success or failure — the lock is released via a best-effort merge update.
| Operation | Concurrency | Mechanism |
|---|---|---|
Lock acquisition (try_acquire_run_lock) |
Atomic — exactly one caller wins | ETag CAS |
Lock release (release_run_lock) |
Best-effort — no ETag | Merge update |
What can go wrong: If a Function host instance is terminated during graph execution (scale-in, deployment, crash), the thread remains in busy status indefinitely because the release never fires.
Recovery: Use reset_stale_locks() from a periodic Timer Trigger to reclaim orphaned locks:
# Reset threads stuck in 'busy' for more than 10 minutes
count = thread_store.reset_stale_locks(older_than_seconds=600)
Each reset uses ETag CAS so a thread that has been legitimately re-acquired since the scan is never stomped. Choose older_than_seconds comfortably above your longest expected graph execution time — ETag CAS protects against re-acquire races, but a still-running long job will not update its updated_at and could be reclaimed if the threshold is too short. See examples/maintenance_timer/ for a complete Timer Trigger wiring.
Stale-lock cleanup caveat:
reset_stale_locksissues a projection query (select=["RowKey", "updated_at"]) and relies on the Azure Tables SDK to expose each row's ETag via eitherentity.metadata["etag"]orentity["etag"]. Rows where neither shape populates an ETag are skipped (logged at DEBUG) so a stale lock is never reset without a writable ETag for CAS; such rows are retried on the next scan once the SDK returns a usable ETag.
Native endpoint thread locking
Native invoke/stream endpoints (POST /api/graphs/{name}/invoke and .../stream) use an in-process per-thread lock when the graph has a checkpointer and the request includes config.configurable.thread_id. This prevents concurrent writes to single-writer checkpointers (e.g. AzureBlobCheckpointSaver) within the same Python worker process.
Important: The default backend is in-process only — it does not coordinate across multiple Function App instances, worker processes, or hosts. Multi-instance deployments (Consumption / Elastic Premium) must either use a distributed
ThreadLockbackend (see below) or route through Platform-compatible runs (platform_compat=True) withAzureTableThreadStore, which provides ETag-based atomic locking.
Distributed thread locking (v0.6+)
The thread_lock parameter accepts any object satisfying the ThreadLock protocol, so multi-instance deployments can swap the default InProcessThreadLock for a distributed backend. The package ships with AzureBlobLeaseThreadLock, which uses Azure Blob lease compare-and-swap as the underlying primitive — no additional infra beyond a Blob container.
import azure.functions as func
from azure.storage.blob import ContainerClient
from azure_functions_langgraph import LangGraphApp
from azure_functions_langgraph.locks import AzureBlobLeaseThreadLock
container = ContainerClient.from_connection_string(
"DefaultEndpointsProtocol=https;AccountName=...", "langgraph-locks"
)
container.create_container() # idempotent — safe on cold start
app = LangGraphApp(
auth_level=func.AuthLevel.FUNCTION,
thread_lock=AzureBlobLeaseThreadLock(container_client=container),
)
| Backend | Distributed? | Infra required | Use case |
|---|---|---|---|
InProcessThreadLock (default) |
No — single Python worker only | None | Local dev, single-instance deployments |
AzureBlobLeaseThreadLock |
Yes — Azure Blob lease CAS | One Blob container | Multi-instance production (Consumption / Elastic Premium) |
Custom ThreadLock implementation |
Yours to define | Yours to provide | Redis, Cosmos DB, Postgres advisory locks, etc. |
Safety guard — AZFUNC_LANGGRAPH_LOCK_BACKEND. Set this environment variable to distributed in multi-instance environments to fail-fast at startup if a distributed backend was not wired. When the value is distributed (or any non-empty value other than inprocess) and thread_lock resolves to the default InProcessThreadLock, LangGraphApp.__post_init__ raises RuntimeError — this prevents accidentally deploying an in-process lock to a horizontally-scaled Function App. See docs/production-guide.md for the full scale-out matrix.
Upgrading
v0.3.0 → v0.4.0
Fully backward-compatible. No breaking changes.
- New optional extras:
pip install azure-functions-langgraph[azure-blob,azure-table]for persistent storage - New platform endpoints: thread CRUD, state update/history, threadless runs, assistants count
- New protocols:
UpdatableStateGraph,StateHistoryGraph(available fromazure_functions_langgraph.protocols)
v0.4.0 → v0.5.0
Fully backward-compatible. No breaking changes.
- Metadata API:
app.get_app_metadata()returns an immutable snapshot of all registered routes and graph info - OpenAPI bridge:
azure_functions_langgraph.openapi.register_with_openapiintegrates withazure-functions-openapi-python - CloneableGraph protocol: thread-isolated graph cloning for safe concurrent execution
When to use
- You have LangGraph agents and want to deploy them on Azure Functions
- You want serverless deployment without LangGraph Platform costs
- You need HTTP endpoints for your compiled graphs with minimal setup
- You want thread-based conversation state via LangGraph checkpointers
- You need durable state persistence with Azure Blob/Table Storage
Documentation
- Start here: 5-minute tutorial — compile a graph → expose as HTTP → run locally → deploy to Azure
- Project docs live under
docs/ - Smoke-tested examples live under
examples/ - Product requirements:
PRD.md - Design principles:
DESIGN.md
Ecosystem
This package is part of the Azure Functions Python DX Toolkit.
Design principle: azure-functions-langgraph owns LangGraph runtime exposure. azure-functions-validation-python owns validation. azure-functions-openapi-python owns API documentation.
| Package | Role |
|---|---|
| azure-functions-openapi-python | OpenAPI spec generation and Swagger UI |
| azure-functions-validation-python | Request/response validation and serialization |
| azure-functions-db-python | SQLAlchemy-powered DB integration helpers (poll-based pseudo trigger, input/output/client injection) |
| azure-functions-langgraph-python | LangGraph deployment adapter for Azure Functions |
| azure-functions-scaffold-python | Project scaffolding CLI |
| azure-functions-logging-python | Structured logging and observability |
| azure-functions-doctor-python | Pre-deploy diagnostic CLI |
| azure-functions-durable-graph-python | Manifest-first graph runtime with Durable Functions (experimental) |
| azure-functions-knowledge-python | Knowledge retrieval (RAG) decorators |
| azure-functions-cookbook-python | Dogfood examples — runnable recipes that exercise the full toolkit |
For AI Coding Assistants
If you are an AI coding assistant (Copilot, Cursor, Claude, etc.), see:
llms.txt— Concise package summary and API overviewllms-full.txt— Complete API reference with signatures, patterns, and examples
Disclaimer
This project is an independent community project and is not affiliated with, endorsed by, or maintained by Microsoft or LangChain.
Azure and Azure Functions are trademarks of Microsoft Corporation. LangGraph and LangChain are trademarks of LangChain, Inc.
License
MIT
Metadata
Release files for azure-functions-langgraph 0.9.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| azure_functions_langgraph-0.9.1.tar.gz | 131.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| azure_functions_langgraph-0.9.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 272.3 kB
Release files / azure_functions_langgraph-0.9.1.tar.gz
| Download URL | azure_functions_langgraph-0.9.1.tar.gz |
|---|---|
| Size | 131.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d1a5833d2d4681412ea2bc3c97e75c5e7f019356a997926d61977c6dfe81a942
|
|
BLAKE2b-256 checksum How to use checksums |
e6f2f1ab3d9a626221ce67330ef570f1c4191b1dfccc87d6a06ec6d79946ecc3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.
Transparency logRelease files / azure_functions_langgraph-0.9.1-py3-none-any.whl
| Download URL | azure_functions_langgraph-0.9.1-py3-none-any.whl |
|---|---|
| Size | 140.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
41600a950ccd9941e9b3dc03526436ab2e22d10cf002b5112cd53e37cc1cefa0
|
|
BLAKE2b-256 checksum How to use checksums |
dc2146d51e0ca1b014e5a30406b1bd0b8442131cea78b9280ad6914be43b1d93
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.
Transparency log