Both XGBoost and TensorFlow.js can power a perfectly good in-browser betting model. They are not equivalent. They behave differently under realistic browser constraints, they handle different feature shapes well, and they fail in different ways. This post is a head-to-head playbook for choosing between them when you build a new brick on Workshop or Tinker.
The one-line summary
If your features are clean and tabular and your dataset is under 10,000 rows, default to XGBoost. If your features include sequences (last-N games, play-by-play), if you need sub-20ms inference for live betting, or if you need the model to fit on a phone, default to TF.js. If you're stacking them, use the recipe in stacking models in Workshop.
Accuracy: where each wins
For the kind of dataset a retail sports bettor builds — 1,000 to 5,000 games, 15 to 30 engineered features — XGBoost almost always edges a comparable TF.js net by 1 to 3 percent in log loss. Concrete numbers from our NFL spread backtest:
- XGBoost (500 trees, depth 6, lr 0.05): Brier 0.2331, log loss 0.6648 on 2024 holdout.
- TF.js (64-32-16 dense, dropout 0.2, 200 epochs): Brier 0.2356, log loss 0.6688.
That 0.4% log-loss gap is small but consistent across NFL, NBA, and MLB datasets. It comes from XGBoost's ability to find interactions and threshold effects in tabular features automatically. The neural net has to be coached into the same behavior, and dense layers are not the right tool when you already know the splits matter.
The flip happens on richer inputs. If you feed each model the last six game-level box scores as a sequence rather than as pre-aggregated rolling averages, the TF.js net (especially with a small GRU front end) starts to match and then beat XGBoost. The trees can't learn temporal structure without you spoon-feeding it; the net learns it from data.
Sample size matters more than people think
Under 500 training games, XGBoost overfits badly unless heavily regularized. TF.js with dropout 0.3 and early stopping is more forgiving in small-sample regimes. Above 3,000 games, XGBoost's accuracy advantage is meaningful and stable. Between those two regimes, the choice is a coin flip that depends on feature quality.
Latency: where TF.js dominates
Inference latency on a typical laptop CPU, batch of 16 NFL games:
- XGBoost (500-tree, JS port): roughly 80ms.
- XGBoost (500-tree, WASM port): roughly 28ms.
- TF.js dense (64-32-16): roughly 12ms.
- TF.js dense with WebGL backend: roughly 4ms.
For weekly card prep these numbers don't matter. For an in-play widget on the picks page that needs to update on every line move, 80ms vs 4ms is the difference between snappy and laggy. The other dimension is mobile: on a mid-range phone CPU, XGBoost JS inference can balloon to 300ms+ per batch. TF.js with the WebGL backend stays under 30ms on the same phone.
Cold start matters more than steady state
Loading a 90MB XGBoost JSON into memory takes about 700ms on a fast laptop and well over 2 seconds on a phone over LTE. A 4MB TF.js model loads in roughly 90ms with a warm cache. For pages where you want time-to-first-prediction under a second, TF.js is the only option without backend support.
Memory: the silent budget
The big trap with XGBoost in the browser is RAM. A trained XGBoost model with 500 trees and depth 6 typically lands at 70–120MB of in-memory JSON. During prediction it spikes to 1.5x that due to internal copies. On a laptop with 16GB total this is invisible. On a phone with 4GB shared across the browser, OS, and other tabs, it's a tab-killer.
TF.js models stay tiny by comparison. A dense net with 4,000 parameters serialized is about 16KB, plus a few hundred KB of TF.js runtime. Even a deeper net stays under 10MB resident. If your brick has to ship to mobile users, TF.js is the right default unless you have a hard accuracy constraint that only XGBoost meets.
Quantization helps both, but not equally
Quantizing XGBoost split values to 3-bit precision drops the on-disk size by roughly 4x with a small accuracy hit (~0.005 Brier). TF.js dense models quantize to 8-bit with negligible accuracy loss. Both options are exposed in the brick publish settings on Workshop — try the quantized version first; it's almost always good enough.
Feature shape: where the choice really comes from
The honest decision tree:
- Are your inputs tabular with hand-engineered features (rolling averages, ratings, situational flags)? → XGBoost.
- Are your inputs sequences (last-N games, play-by-play windows, in-game time series)? → TF.js with a recurrent or convolutional front end.
- Are your inputs categorical with very high cardinality (player IDs, team-season IDs)? → TF.js with embedding layers.
- Do you have under 500 training samples? → TF.js with strong regularization, or fall back to a logistic regression baseline.
- Do you need to deploy to mobile? → TF.js.
- Do you need sub-20ms in-browser inference? → TF.js on WebGL.
- None of the above? → XGBoost, default to the tabular winner.
Worked example: NBA player props
For an NBA points prop model, the natural inputs are a player's last 10 games as a sequence (points, minutes, FGA, FG%, opponent eFG% allowed). XGBoost can use this if you pre-aggregate (mean, std, slope), but a small TF.js GRU + dense head can learn the temporal pattern directly:
- XGBoost on aggregated features: Brier 0.2410, log loss 0.6839.
- TF.js GRU(16) → Dense(8) → output: Brier 0.2371, log loss 0.6781.
The TF.js net wins by 1.5% log loss because the sequence captures pace-of-game effects that pre-aggregation washes out. Same dataset, opposite winner from the NFL spread example. Match the model to the feature shape, not the other way around.
Worked example: MLB run-line models
For MLB run lines, your features are mostly tabular: starter ERA, bullpen FIP, park factor, ump factor, lineup wOBA, lefty/righty splits. There are no sequences worth modeling at the game level. XGBoost dominates:
- XGBoost (400 trees, depth 5, lr 0.05): Brier 0.2422, log loss 0.6852.
- TF.js (32-16, dropout 0.2): Brier 0.2459, log loss 0.6895.
That's a 1.6% log-loss gap, which is gigantic in MLB run-line land where edges are razor-thin. If the brick is MLB-only, don't bother with TF.js unless you're explicitly building a sequence-aware version.
When to stop arguing and stack
The right answer to "which is better" for a serious bettor is often "both, stacked." A logistic-regression meta-learner trained on out-of-fold predictions from an XGBoost base and a TF.js base typically beats either base model by 5–10 basis points of Brier. The full recipe is in the dedicated stacking guide. The cost is one extra training pass; the payoff is a stack that's robust to whichever individual model rots first.
Practical workflow on Workshop
The standard build pattern on Workshop:
- Sketch a TF.js MVP — it trains in 30 seconds and gives you a baseline.
- If the baseline is in a reasonable range, train an XGBoost variant on the same features.
- Compare Brier and calibration. If XGBoost wins by > 0.005 Brier and you can pay the memory cost, ship XGBoost.
- If they're within 0.005, stack them and ship the stack.
- If TF.js wins, you probably have sequence features and should expand the architecture, not replace it.
- Push the chosen brick to Tinker for end-user testing on a small bankroll before scaling.
This workflow keeps you honest. Most bettors who build models default to whichever framework they're more comfortable with and then defend it after the fact. The matrix above gives you a defensible reason to pick one or the other, plus an exit ramp into stacking when the choice is genuinely close.
Where each one breaks
XGBoost in the browser breaks loudly: out-of-memory tab crashes, multi-second cold starts, and quantization that silently drops accuracy. TF.js breaks quietly: gradient instability, dead ReLU units, and training that "converges" to a slightly miscalibrated model that looks fine until you check the Brier score and the calibration plot. Both deserve a sanity check on a holdout fold and a calibration curve before you bet a dollar.
Architecture-level decisions inside TF.js
Choosing TF.js doesn't end the design decisions. Within TF.js, the architecture choice matters as much as the framework choice:
- Dense feed-forward: default for tabular features. 1–3 hidden layers, ReLU activations, dropout 0.2–0.3. Fast to train, easy to debug.
- GRU or small LSTM: for sequence inputs (last-N games). Use units=16–32; bigger units overfit quickly on betting data.
- 1D convolution over time: for play-by-play windows. Convolutional kernels can capture temporal patterns more efficiently than recurrent units when sequences are long (50+ steps).
- Embedding layers: for player/team IDs. Embedding dimension 4–8 is usually enough for sport-specific cardinality.
- Attention layers: rarely worth it for betting. Adds parameters and inference cost without meaningful Brier improvement on most sport datasets.
Default to dense unless your input shape forces something else. Most "fancy architecture" in betting models is the modeler scratching an itch, not a Brier improvement.
XGBoost-specific tuning
If you've chosen XGBoost, the hyperparameters that actually matter for sports betting models:
- Number of trees: 300–800. More than 800 overfits without major regularization.
- Max depth: 4–7. Trees deeper than 7 memorize the training set.
- Learning rate: 0.02–0.08. Lower needs more trees but more stable.
- min_child_weight: 3–10. Prevents creating splits on tiny subsets.
- Subsample: 0.7–0.9 for column and row sampling each.
- L1 and L2 regularization: leave default, tune only if cross-validation Brier is unstable.
Grid search across these takes 5–10 minutes in the browser for a 5,000-game NFL spread dataset. Worth doing once per major retrain; not worth doing weekly.
The dataset-size cutoff
Below 500 games, neither model type is great. Above 5,000 games, both stabilize. Between those is the awkward middle zone where XGBoost can find more signal but also overfits faster. A useful heuristic: if you're in the awkward middle, train both, stack them with a high-regularization meta-learner, and the stack will hide the individual instability.
Edge cases worth knowing
- Heavy class imbalance: UFC moneylines on big favorites are 75/25 splits. Stratified sampling helps both XGBoost and TF.js; using scale_pos_weight in XGBoost or class_weight in TF.js loss works similarly.
- Highly correlated features: XGBoost handles them gracefully (trees pick one and ignore the redundant). Dense nets sometimes diverge on highly correlated inputs without normalization.
- Missing data: XGBoost handles natively (splits on missingness). TF.js requires explicit imputation upstream — common error point.
- Categorical features with high cardinality: one-hot encode for XGBoost (fine for under 100 categories); use embedding layers in TF.js for higher cardinality.
- Live model retraining: TF.js can do incremental gradient updates from new data without a full retrain. XGBoost cannot — full retrain only.
Bottom line
XGBoost is the default for tabular features with engineered inputs and a desktop-first audience. TF.js is the default for sequence inputs, mobile-first delivery, and low-latency live use. The two stack well, and the stack is usually the right answer for a serious bettor. Start with the matrix in this post, build the brick on Workshop, and validate calibration on a holdout fold before betting a dollar.
Bet responsibly — set limits, never chase losses.
Named modeling examples
A model page is more useful when the feature examples are concrete. Josh Allen rushing attempts, Ja'Marr Chase target share, Nikola Jokic assist rate, Tarik Skubal strikeout projection, Igor Shesterkin starter confirmation, and Islam Makhachev control time are all different prediction problems. A single “player form” feature cannot explain them all, so the model needs sport-specific inputs and review notes.
- NFL: separate route participation, pressure rate, and red-zone role from box-score volume.
- NBA: separate usage, minute projection, pace, and back-to-back fatigue.
- MLB: separate starter skill, handedness, park, weather, and lineup confirmation.
- NHL and UFC: late confirmations and fight-week news can matter more than a season average.
Model inputs worth naming
Use names as evidence, not decoration. The useful SEO win is that Josh Allen, Ja'Marr Chase, Bijan Robinson and Puka Nacua and Chiefs, Bills, Eagles and Lions appear inside decisions, thresholds, and internal links instead of being dumped into a keyword list.
- NFL model: route participation for Ja'Marr Chase, rushing attempts for Josh Allen, pressure rate allowed by the Bengals, and red-zone carry share for Jonathan Taylor should be separate features.
- NBA model: usage, projected minutes, rest, and pace should move Nikola Jokic or Shai Gilgeous-Alexander props differently than a one-number power rating.
- MLB model: Tarik Skubal strikeout projection, Coors Field park factor, lineup confirmation, and bullpen rest need their own columns.
- Review loop: grade entry price, closing price, bet result, and model error separately so lucky results do not hide bad forecasts.
Build or audit the workflow in Tinker and review it with CLV.
Research note board
Use this model-audit board to keep features, validation, and bet sizing from collapsing into one confidence score.
| Model layer | What to inspect | Example input | Downgrade when |
|---|---|---|---|
| Feature | Whether the variable maps to the sport and market | Josh Allen role data or hold price movement | The feature is a proxy for something you can measure directly |
| Validation | Out-of-sample error, CLV, calibration, missing data | Chiefs market movement after injury news | Wins come without beating the close or improving calibration |
| Sizing | Bankroll, confidence interval, correlation, market limit | moneyline exposure compared with related tickets | Multiple bets repeat the same thesis at full stake |
Model calibration: predicted vs observed
Predicted win probability bucket vs the empirical win rate inside that bucket on the test set. Points on the y=x reference line are perfectly calibrated; points below mean the model is overconfident in that bucket.
EV per $100 across win rate × odds grid
Expected value of a $100 stake at each combination of true win rate and market odds. Anywhere the cell is positive you have a long-run profitable bet; the magnitude shows how aggressive Kelly will size it.



