Shared environment & billing
The shared environment is the primary way to ship a game on Crowded Kingdoms: create your app on the shared platform and your players connect to a single, managed Game API scoped by your appId. You do not provision or run any infrastructure.
The shared platform is generally available. Create an app through Get started or createApp with shared deployment — it is immediately active after create. Sandbox and production follow the same model.
There are two deployment models in the platform:
- Shared environment (customer default) — your app runs on the shared Game API, scoped by your
appId. Free to start; you pay only for metered usage above a free allowance. - Dedicated environment — retired without replacement. There is no customer-provisioned
stack and no API for one; contact Crowded Kingdoms if you need enterprise isolation.
Every app uses the shared platform (
publishAppToShared).
So in practice there is one model, and this page describes it.
Free tier
- Every organization can create up to
platformConfig.freeAppsPerOrgapps on the shared environment (default 3). - Each app includes a monthly development quota, per UTC calendar month: 5 GB of client egress, 5 GB of client ingress (decimal GB: 1 GB = 1,000,000,000 bytes), 20 CPU-hours of compute pooled across GraphQL resolvers and your app's ck-exec code, 1 GB-month of stored data, and 1 GB of compute-module writes (what your hubs write when they save their state). Egress is the headline number and the one most games reach first. Unused quota does not roll over.
- The quota runs a small game's hubs unfunded. A hub saves its state on every persist interval, so its writes are roughly its state size times the persist rate times the hours it runs: a four-player game with about six hubs holding 100 KB between them, saving every 30 seconds, writes about 12 MB per hour of play, so the 1 GB covers roughly 80 hours a month. Writes are charged by the byte only; how many rows a save touches does not matter.
- ck-exec instance limits follow funding. An app whose organization cannot be charged (no spendable wallet balance, no auto-billing) may run 16 hub instances reserving 1 GB of memory per datacenter at once; an organization that can be charged gets 16,384 instances and 64 GB. Past either limit the next instance is refused until some stop; running ones are left alone. See limits.
- Stored input logs have no free allowance. An app records nothing until an
org member with
manage_appsturns on replay logging (replayLoggingEnabled: trueonupdateApp), which is refused withINPUT_LOG_FUNDS_NEEDEDunless the org wallet can spend the projected cost of keeping one retention period of the app's recent traffic. Every message a client sends is recorded once it is on, keep-alives included (about 50 to 110 bytes a second for a connected player who does nothing), and recordings already kept go on being billed after it is turned off, until they age out. See Input logging. - You are billed for bytes, CPU and storage — never for counts. A datagram, an API operation, a notification or a row a hub writes is paid for by the bytes it moves or writes and the CPU it takes; there is no per-message, per-operation, per-notification or per-row rate on the card (the message and operation counts were retired on 2026-09-06, rows written by compute modules on 2026-10-12).
- Within the quota the app runs at no cost. Above it, usage is billed from your organization wallet at the published rate card as it arrives: each dimension's month-to-date total is rounded up to the next whole cent once, and the wallet is debited as the total crosses each cent, within about a minute of the usage. The hourly rows on the Plan and Usage tab itemise what was used and what was charged during each hour; they are a statement, not a separate charge.
- A free app that has never been funded is also shaped to roughly 1 MB/s of egress. That cap is lifted by funding the org wallet or enabling auto-billing — not by buying a reservation. Once an organization can be charged, CK does not rate-limit its egress, and spend is bounded by the caps you set rather than by a throughput ceiling.
What counts as a billed byte. Egress only: bytes the service delivers to your
clients. Ingress — what your clients send — is metered and visible in usage, but
does not count toward the monthly volume or its allowance. Volume is measured as
wire bytes at the network interface, so it includes the transport and network
headers each frame carries, and it is counted after any compression the service
applies. A MESSAGE_BUNDLE datagram carrying several messages is one frame and is
metered once, in either direction. A client-side byte counter will not match it. The full basis is in the
Free Tier and Billing Basis.
What counts as a stored input-log byte. input_log_storage_byte_hours is
measured in recorded bytes: each input counts at its record size — the
sizeBytes that inputLogMessages returns, a fixed header plus the message —
before compression and replication, so you can add it up yourself. The header is
48 bytes since the replication server release after v0.37.0 (40 before), which is
what a recorded spatial message's fixed overhead of 116 bytes is made of (with its
68-byte spatial header). Each byte is
billed for exactly the input log's retention (7 days, 168 hours), so 1 GB
recorded is 168 GB-hours, 0.23 GB-month; it is priced per GB-month on the rate
card in your account and on the
pricing page. appUsageSummary
reports an app's byte-hours and the bytes it holds now, and
appUsageProjection the month so far and a projection.
Volume bands. A line on the rate card can be priced in graduated volume
bands, and egress is: the more an app sends in a UTC calendar month, the lower
the rate on its next gigabyte. Each byte is charged at the rate of the band it
falls in, so reaching a higher band does not re-price the bytes below it. The
bands count each app's own egress from the start of the month, free quota
included (the 5 GB quota is the bottom of the first band), and the month's
total is still rounded up to the cent once. An app's rate for the month is set
at its first charge in that month, so a change to the rate card reaches an app
from its next month. The schedule is on the rate card in your account and on
the pricing page. In the API,
meteredRateCard lists a banded line's priceBands: each band's priceCents
per the line's unitQuantity (a GB, for egress) and its upper edge
upToUnits in raw units (bytes, for egress; null on the last, open-ended
band). The line's own priceCents is the first band's rate, and a line priced
at one rate has no priceBands.
An app's ck-exec code is metered minute by minute in compute units: the greater of the CPU time and the fuel each minute used, so neither a host stall nor unusually dense guest instructions under-report work. A CPU-hour on the rate card is one core busy for one hour, and the price is the rate card in your account and on the pricing page. Realtime events the code sends count as replication egress, billed through the same monthly egress aggregate as every other byte your app sends. See usage and budgets.
Check your remaining free slots:
query {
orgFreeAppQuota(orgId: "123") {
quota
usedFree
paidApps
remainingFree
}
}
Creating an app (shared by default)
New apps created through the Management UI Get started wizard or createApp go live on the shared Game API immediately. datacenter is required: a code that placeableDatacenters lists as placeable.
mutation {
createApp(input: {
orgId: "123"
name: "My Game"
slug: "my-game"
datacenter: "or"
}) {
appId
deploymentTarget
runtimeStatus
gameApiUrl
}
}
When create succeeds, expect:
deploymentTarget=sharedruntimeStatus=activegameApiUrl= the tier's shared endpoint (discover viaplatformConfigif unset in the response)
No separate provisioning step or publishAppToShared call is required for new shared apps.
Publishing legacy or migrated apps
If you have an app that was created before shared-by-default, you can still publish it explicitly:
mutation {
publishAppToShared(appId: "456") {
appId
free
}
}
Publishing is free. There is no paid app slot and no per-app fee (API Terms of
Service §3.7): every app runs on the shared environment, and what you pay for is
the usage it meters, drawn from your organization wallet as described below. The
planId and provider arguments still accepted by publishAppToShared, and the
sharedEnvPlans catalog, are deprecated leftovers of the retired slot model and
are ignored; do not pass them.
Paying for usage
Usage is billed from your organization wallet (a prepaid balance). Top the wallet up with a checkout, then usage above the free allowance is debited automatically.
Spend caps
Cap how much an app can spend per hour and/or per day. Once a cap is reached the app is denied until the window resets (or you raise the cap):
mutation {
setAppSpendCaps(appId: "456", hourlyLimitCents: "500", dailyLimitCents: "5000") {
runtimeStatus
runtimeDenialReason
}
}
Pass null for a limit to clear it.
Auto-billing
Instead of (or in addition to) manual top-ups, an org owner can enable auto-billing: when the wallet can't cover usage, the saved payment method is charged automatically to top the wallet back up — up to a limit you set, or with no limit. With auto-billing off, an app is simply denied when the wallet runs dry; with it on, the app keeps running until your auto-billing limit is reached.
mutation {
setAutoBilling(orgId: "123", enabled: true, limitCents: "10000") {
enabled
limitCents
autoBilledThisPeriodCents
}
}
Set limitCents: null for no limit. Add a card first with
setupSharedPaymentMethod.
Reserved capacity
Reserving capacity is optional and separate from paying for usage. It asks the platform to provision and hold a minimum for your app, and it is sold in two independent dimensions because a game may need a great deal of one and little of the other:
- Realtime (UDP) throughput, in bytes/sec. Set with
setAppReservedThroughput; read back asapp.reservedUdpBytesPerSec. - API request rate, in GraphQL operations/sec, as
app.reservedGraphqlOpsPerSec.
Reserving one does not reserve the other.
Three things a reservation is not:
- Not a ceiling. It obliges CK to keep that much capacity in service for you; it does not cap what you may send. Use above the reserved rate is metered like any other usage rather than refused.
- Not a data allowance. The monthly fee buys capacity, not volume. It is charged in addition to metered usage and includes no bytes of its own — reserving 5 MB/s does not make the first 5 MB/s free.
- Not how you lift the free-tier cap. Funding a wallet does that (see Free tier). Before 2026-09-01 a reservation doubled as a rate-limit bypass; it no longer does.
Billed monthly from the org wallet whether or not the capacity is used; upgrades
are prorated for the current month, and lowering or clearing a reservation charges
nothing. Requires manage_billing on the app's organization.
What "access denied" means
When an app can't serve traffic, appRuntimeState reports a runtimeStatus
other than active and a runtimeDenialReason:
insufficient_funds— the app needs to be paid for and cannot be: it is past a free allowance and the wallet is empty with auto-billing unable to cover it. Top up the wallet or enable auto-billing. (An app still inside its allowances is not denied at all, so this reason covers both "out of allowance" and "out of money" — they are the same situation.)spend_cap— an hourly/daily spend cap was reached. Raise the cap or wait for the window to reset.subscription_lapsed— retired with the paid app slot; no current app can carry this reason. Listed because the enum still names it.
query {
appRuntimeState(appId: "456") {
deploymentTarget
runtimeStatus
runtimeDenialReason
walletBalanceCents
currentHourUsageCents
currentDayUsageCents
hourlyLimitCents
dailyLimitCents
}
}
While an app is denied, nothing is delivered for it, and every path tells your client why so it can say "this world is paused" instead of showing an empty one:
- Sign-in still works.
mintAppTokenandrefreshAppTokenreturn the token with aruntimeGate(statusandreason; no wallet figures), andgameClientBootstrapreturns the sameruntimeGateinstead of failing. - Realtime refuses. The GraphQL realtime proxy refuses to connect with
APP_PAUSED(carrying the reason). A direct or relayed session gets a generic error with code 33 (APP_PAUSED) in answer to a send, at most once every few seconds per session; see error codes. - Hubs pause. The app's ck-exec code is switched off entirely, and a refused call names the cause ("app 456 is paused: its organization has no funds (insufficient_funds)").
Everything resumes by itself within a minute of the app becoming active again:
fund the wallet, raise the cap, or renew.
Connecting clients
Clients discover the shared Game API URL programmatically — never hard-code tier-specific hosts in production.
The public platformConfig query (no auth) returns the shared endpoints:
query {
platformConfig {
sharedGameApiUrl
sharedGameApiWsUrl
freeAppsPerOrg
}
}
Query the app directly for routing fields — gameApiUrl is set for shared apps after create:
query {
app(appId: "456") {
appId
deploymentTarget # "shared"
runtimeStatus
gameApiUrl # shared endpoint for this tier
}
}
mintAppToken returns gameApiUrl / gameApiWsUrl alongside the app-scoped gameplay token — the most convenient path when wiring clients after sign-in.
Point your client (or a second CrowdyClient) at that gameApiUrl, and pass the
same appId when you open the realtime subscription — the session is app-scoped.
For the SDK walkthrough see
Loading an app's Game API.