Speed Format (kb vs M)
Internally, every speed value in ProxRad is in kilobits per second (kb / kbps). A download_speed of 2000 means 2 Mbps, not 2 kbps and definitely not 2 Gbps. This page documents the rule, the conversion helper that enforces it on the way out to MikroTik, and the production incident that resulted in customers getting 1000× their plan speed for a few hours.
If you write code that touches a speed column, read this page first. The convention is rigid; getting it wrong is one of the most expensive bugs in the codebase.
The rule
Section titled “The rule”| Column / variable | Unit | Example value | Means |
|---|---|---|---|
services.download_speed (INT) | kb/s | 2000 | 2 Mbps |
services.upload_speed (INT) | kb/s | 1200 | 1.2 Mbps |
services.download_speed_str (VARCHAR) | string with k suffix | "2000k" | 2 Mbps |
services.upload_speed_str (VARCHAR) | string with k suffix | "1200k" | 1.2 Mbps |
services.burst_* (string) | string, k or M accepted on input | "3M" | 3 Mbps |
RADIUS Mikrotik-Rate-Limit | string up/down (always normalized) | "1200k/2000k" | Up 1.2 Mbps, down 2 Mbps |
| Frontend input labels | Read “kb” explicitly | ”Download Speed (kb)“ | Force the operator to think in kb |
The frontend labels were renamed from “Mbps” to “kb” specifically to stop operators typing 2 and meaning 2 Mbps when the field expects 2 kbps.
The conversion helper
Section titled “The conversion helper”normalizeSpeedForMikrotik in the backend converts any input shape to canonical kb format for the Mikrotik-Rate-Limit attribute:
// normalizeSpeedForMikrotik converts all speeds to kb format.// Examples: "1.2M" -> "1200k", "2M" -> "2000k", "1200k" -> "1200k"func normalizeSpeedForMikrotik(speed string) string { if speed == "" { return "" } speed = strings.TrimSpace(speed) lowerSpeed := strings.ToLower(speed)
// Already in k format - return as-is if strings.HasSuffix(lowerSpeed, "k") { return speed }
// Convert M to k if strings.HasSuffix(lowerSpeed, "m") { numPart := speed[:len(speed)-1] val, err := strconv.ParseFloat(numPart, 64) if err == nil { return fmt.Sprintf("%dk", int(val*1000)) } return speed }
// Plain number - add k suffix return speed + "k"}Conversion examples:
| Input | Output |
|---|---|
"2M" | "2000k" |
"1.2M" | "1200k" |
"5.5M" | "5500k" |
"512k" | "512k" (unchanged) |
"2048" | "2048k" (plain number gets k appended) |
"100K" | "100K" (already-suffixed, case preserved) |
"" | "" |
There is no G (gigabit) suffix. RouterOS itself doesn’t accept G in Mikrotik-Rate-Limit; if anyone ever needs a gigabit speed, write "1000000k" (1 Gbps). At the time of writing, the largest deployed plan is 100 Mbps = "100000k".
How the rate-limit string is assembled
Section titled “How the rate-limit string is assembled”The full Mikrotik-Rate-Limit value can have up to four space-separated fields, each in up/down form:
"1200k/2000k 2400k/4000k 3000k/5000k 30/30" | | | | base burst-rate burst-threshold burst-time-up/downMost plans use only the first field. Burst settings are added when the service has burst_* columns set. normalizeRateLimitString iterates each space-separated field, splits on /, normalizes each half, and reassembles — so a service stored as "2M/5M 3M/7M" becomes the wire value "2000k/5000k 3000k/7000k".
The v1.0.387 incident — the cost of guessing wrong
Section titled “The v1.0.387 incident — the cost of guessing wrong”Pre-v1.0.387, several handlers and services in the codebase assumed services.download_speed was in Mbps and multiplied by 1000 before formatting:
// WRONG — assumed Mbps, multiplied by 1000rateLimit := fmt.Sprintf("%dk/%dk", sub.Service.UploadSpeed*1000, sub.Service.DownloadSpeed*1000)For a 2 Mbps plan stored as download_speed = 2000 (kb), this generated "2000000k/2000000k" — 2 Gbps. RouterOS happily accepted the value and rate-limited the queue to 2 Gbps, which on a 100 Mbps WAN circuit means unlimited speed.
The bug lived in multiple places — bulk Renew, bulk Reset FUP, FUP individual reset, bandwidth_rule_service, cdn_bandwidth_rule_service, quota_sync’s restoreOriginalSpeedIfNeeded — anywhere code touched a speed value after reading it from services. The fix was a search-and-destroy on * 1000 patterns near speed columns. The correct form is:
// CORRECT — speeds already in kb, no multiplicationrateLimit := fmt.Sprintf("%dk/%dk", sub.Service.UploadSpeed, sub.Service.DownloadSpeed)How the frontend handles input
Section titled “How the frontend handles input”The frontend lets operators type in M or k and the backend normalizes on save. The handler convertSpeedForMikrotik (in the backend) accepts:
- Plain numbers:
2000→ stored as"2000k". - Numbers with
ksuffix:2000k→ stored as"2000k". - Numbers with
Msuffix:2M→ stored as"2000k". - Decimal
M:1.5M→ stored as"1500k".
Storing as a string preserves the operator’s intent and lets normalizeSpeedForMikrotik see the format on its way out. The numeric download_speed column is also populated from the same input — both sides agree.
Burst formula consistency
Section titled “Burst formula consistency”Time-based bandwidth boosts apply directly to the kb integer, no conversion:
// 4000k base + 100% boost = 8000kdownloadK := baseDownloadK * (100 + int64(service.TimeDownloadRatio)) / 100A 4 Mbps plan with a 100% NIGHT boost becomes 8 Mbps = "8000k". This is the formula used by the backend’s checkAndApplyTimeBasedSpeed and the backend’s applyRuleToNasSubscribers. Both were corrected in the v1.0.149 cleanup that paralleled v1.0.387.
What MikroTik does with the value
Section titled “What MikroTik does with the value”RouterOS parses the Mikrotik-Rate-Limit VSA and constructs a simple queue on the PPPoE interface with these limits. Internally MikroTik stores rates in bps (bits per second), but the wire format is always the human-readable k or M suffix.
To confirm a value made it through correctly, on RouterOS:
/queue simple print where target="<pppoe-user>" detailThe max-limit= field shows the actual limits in upload/download bps. 2M/4M = 2,000,000 / 4,000,000 bps = 2 Mbps / 4 Mbps. If you see 2G/4G (2 Gbps / 4 Gbps) — you’ve reproduced the v1.0.387 incident.
Common workflows
Section titled “Common workflows”Plan a new speed tier
Section titled “Plan a new speed tier”- Decide the plan in Mbps as marketing thinks of it — e.g. “10 Mbps down, 4 Mbps up.”
- Translate to kb in your head — multiply by 1000. 10 Mbps = 10000 kb. 4 Mbps = 4000 kb.
- In Services → New Service, enter:
- Download Speed (kb):
10000 - Upload Speed (kb):
4000
- Download Speed (kb):
- Save. The backend stores
download_speed=10000, download_speed_str="10000k". The RADIUS reply will setMikrotik-Rate-Limit = "4000k/10000k".
Diagnose a speed mismatch
Section titled “Diagnose a speed mismatch”- What does the database say?
SELECT download_speed, download_speed_str FROM services WHERE id = N; - What did RADIUS send? Look in the radius logs for the Access-Accept:
docker logs proxpanel-radius 2>&1 | grep "Mikrotik-Rate-Limit". - What’s on the router? SSH to MikroTik,
/queue simple print where target="<user>". Themax-limitfield shows what the router applied. - The three should agree. If radius logs show
2000k/4000kbut the queue shows2M/4M(which is the same), you’re fine. If logs show2000000k(a million kbps = 1 Gbps), you’ve hit v1.0.387.
Related pages
Section titled “Related pages”- RADIUS Server Setup —
Mikrotik-Rate-Limitis one of the reply attributes. - MikroTik Integration — how RouterOS parses the value.
- CoA & Disconnect — mid-session rate-limit updates carry the same string.
- Services — UI where speeds are entered.