Deploying a Model on Azure OpenAI: Resource, Deployment and Quota
10 min read · updated August 11, 2026
The thing you create in the Azure portal is not the thing you call. Getting a model answering requests means creating two Azure objects with two different lifecycles, two different billing behaviours and two different ways of running out.
Two objects, not one
An Azure OpenAI resource is an ARM resource of type Microsoft.CognitiveServices/accounts with kind: OpenAI. It owns a region, an endpoint hostname, a pair of API keys and a set of role assignments. It does not own a model, and on its own it will not answer a single inference request. Creating one costs nothing.
A deployment is a child resource, Microsoft.CognitiveServices/accounts/deployments, that binds one model at one version to a name you choose and a capacity you request. That name is what goes in the request path. The model name does not appear in the URL at all, which is why you can deploy the same model twice under two names with two different rate limits, and why moving a workload to a newer model can be a deployment change with no application change.
The consequence people trip over: quota is consumed by the deployment, not by the resource. Microsoft documents quota as assigned per subscription, per region, per model and per deployment type, and each deployment draws that pool down by whatever capacity it was created with. A deployment sitting idle at 200K TPM is holding 200K TPM away from every other deployment of that model in the region, forever, at no charge and with no warning.
Create the resource
One flag on this command matters more than the rest. A --custom-subdomain-name gives the resource a unique hostname of the form https://NAME.openai.azure.com/ rather than a shared regional endpoint, and Microsoft documents custom subdomains as required to enable features such as Microsoft Entra ID authentication. Private endpoints need one too. It cannot be changed afterwards — Microsoft’s custom subdomain guidance is explicit that reusing a name requires deleting the resource that holds it — so decide it now rather than rebuilding in three months when somebody asks for keyless auth.
az cognitiveservices account create \ --name mg-openai-weu \ --resource-group rg-inference \ --location westeurope \ --kind OpenAI \ --sku S0 \ --custom-subdomain-name mg-openai-weu \ --yes
Create the deployment
Deployments are managed on ARM control-plane API version 2023-05-01, which is unrelated to the api-version you pass on inference calls. Two version numbers, two planes, and mixing them up produces a 404 that looks like a missing deployment.
az cognitiveservices account deployment create \ --resource-group rg-inference \ --name mg-openai-weu \ --deployment-name chat-default \ --model-name gpt-4.1 \ --model-version "2025-04-14" \ --model-format OpenAI \ --sku-name "GlobalStandard" \ --sku-capacity 100
--sku-name is the deployment type and it decides where your tokens are processed. Microsoft documents Standard (single region), DataZoneStandard (routed within a US or EU data zone), GlobalStandard (routed across Azure’s global fleet), and the provisioned equivalents ProvisionedManaged, DataZoneProvisionedManaged and GlobalProvisionedManaged. These are separate quota pools. Having headroom on GlobalStandard tells you nothing about whether a Standard deployment will be accepted.
The same object in Bicep, which is how it should exist if anything about it is meant to survive:
resource chatDeployment 'Microsoft.CognitiveServices/accounts/deployments@2023-05-01' = {
parent: openAiAccount
name: 'chat-default'
sku: {
name: 'GlobalStandard'
capacity: 100
}
properties: {
model: {
format: 'OpenAI'
name: 'gpt-4.1'
version: '2025-04-14'
}
}
}What capacity actually buys
For a standard deployment, Microsoft documents capacity in units where a value of 1 equals 1,000 tokens per minute — so capacity: 100 above is a 100K TPM deployment. For a provisioned deployment the same field is a count of provisioned throughput units instead, which is why the two cannot be compared by eye.
The part that bites automation is the requests-per-minute limit you get alongside it, because you do not set it. Microsoft’s quota management article publishes the conversion as a per-model table, and the ratios are not close to each other: older chat models get 6 RPM per 1,000 TPM, o3 and o4-mini get 1 RPM per 1,000 TPM, and o3-mini gets 1 RPM per 10,000 TPM. Microsoft flags this specifically as a hazard for programmatic deployment: a template that hard-codes a capacity figure copied from one model will silently allocate a wildly different request budget when pointed at another.
Call it
The deployment name — not the model name — is the path segment. With the OpenAI Python SDK against an Azure endpoint, the model argument is overloaded to mean the deployment:
from openai import AzureOpenAI
client = AzureOpenAI(
azure_endpoint="https://mg-openai-weu.openai.azure.com/",
api_key="<key>",
api_version="2024-10-21",
)
response = client.chat.completions.create(
model="chat-default", # the deployment name, not "gpt-4.1"
messages=[{"role": "user", "content": "Summarise this in one line."}],
max_tokens=200,
)
print(response.choices[0].message.content)Keep max_tokens honest here. It is not only a truncation control: Microsoft documents the rate-limit estimate as being computed from prompt size plus max_tokens at the moment the request arrives, before any tokens are generated. A default of 4,000 on a workload that returns 200 tokens spends twenty times the rate budget it needs, which is the single most common cause of a 429 that makes no sense against your usage metrics.
If this returns a 404, work through three causes in order. The deployment name is case-sensitive and is the name you chose, not the model — gpt-4.1 in the model field is the classic mistake, and it produces a deployment-not-found error rather than anything mentioning models. The api-version may predate the feature you are using, in which case the route itself does not exist on that version. And the endpoint may be the regional hostname rather than your custom subdomain, which resolves and answers but knows nothing about your resource. None of the three produces an error that names the actual problem, so check all three rather than the first.
Deleting things in the wrong order
- Delete the deployments first. Microsoft documents that portal deletion of a resource is blocked while deployments still exist, precisely so that quota is released properly.
- Then delete the resource. Deleting it through the REST API or another programmatic path bypasses that check — and Microsoft documents that when this happens the quota allocation stays unavailable for 48 hours until the resource is purged.
- If you are already stuck in that state, trigger an immediate purge of the soft-deleted resource rather than waiting it out. This is the usual cause of “I deleted everything and still cannot create a deployment”.
It is worth building the teardown before you need it. A Terraform destroy that removes the account and leaves the deployment records implicit will hit exactly this path, and a CI pipeline that creates and destroys a test deployment on every run will exhaust a region’s quota in two days while appearing to clean up after itself perfectly.